简介
系统的混乱并非业务本身之复杂,我们并不擅长处理『简单』小时候打台球,见到能翻袋,下底,长杆的人真厉害,幻想自己有一天能厉害到怎样难的球都能打进。长大后发现,真正厉害的人,母球永远不会轻易停在难打的地方。有能力解决复杂问题的人,我们给予掌声,而能够持续保持简单保持纯粹,应该得到更高的赞许。
本文主要来自对阿里巴巴高级技术专家张建飞 公众号文章《从码农到工匠》的梳理和总结。作为技术人员,我们不能忘记我们技术人的首要技术使命是治理软件复杂度。
关于原则和方法论,既不必刻意拔高,也不要嗤之以鼻。指导实践的不是更多的实践,而是实践后的总结和思考。
- 软件复杂性第一定律:设计良好的系统必然会随着时间的推移退化为设计不良的系统。
- 一个设计良好的系统是指易于随时间改变的系统;一个设计不良的系统是指难以改变的系统。
- 软件复杂性二定律:复杂性是一道护城河(充斥着漏洞百出的抽象)。
- 一个良好的抽象是在为应用程序提供实用性和隐藏实现细节之间取得平衡。当系统在市场份额上竞争时,这种微妙性就会被放弃,设计者往往会给予应用程序所有它想要的东西。观察世界上最成功的系统,都具有几乎不可能以任何其他方式实现的 API(ZooKeeper 的强于线性一致性和基于 TCP/IP 的临时节点语义,Kafka 的幂等生产语义等)。
- 根据这一定律,大多数工程师将会在设计不良的系统上工作,因为大多数成功或流行的系统都是设计不良的系统。
- 软件复杂性第三定律:软件复杂性没有根本上限。
- 在由大型团队随时间构建的现实世界系统中,复杂性仅受人类创造力的限制。系统的形态由几十个开发人员的能力、理念和个性决定,他们每个人都在复杂的真实和感知的激励下工作。每个现有系统都是几十个你可能不认识的人的对你的一次 DoS 攻击;一个在你参与项目之前就已经充满复杂性定时炸弹的宫殿。
如何定义复杂性
“复杂” ( Complexity )定义为由于组件之间的依赖关系、关系和交互,而难以对其行为建模的任何系统。更通俗地说,复杂系统的“整体”大于“部分”之和(大的越多越复杂)。也就是说,如果不查看单个组件以及它们如何相互作用,就无法理解其整体行为的系统,同时也无法通过仅查看单个组件而忽略系统影响来理解系统的整体行为。
系统困境与软件复杂度,为什么我们的系统会如此复杂关于复杂的定义有很多种
- 理性度量,McCabe 圈复杂度,罗列了各种维度,从而判断软件当前的开发/维护成本。
- 感性认知,所谓复杂性,就是任何使得软件难于理解和修改的因素。表现
- 变更放大,看似简单的变更需要在许多不同地方进行代码修改
- 认知负荷,开发人员需要多少知识才能完成一项任务
- 未知的未知,开发人员必须获得哪些信息才能成功地执行任务
为什么会产生复杂性 系统越来越复杂了,像是宿命
- 想简单图省事,没有及时治理不合理的内容
- 缺少匠心追求,对肮脏代码视而不见
- 技术能力不够,无法应对复杂系统
- 交接过渡缺失,三无产品几乎无法维护
除了上述内容外,还可以想到很多理由。但我们发现他们好像有一个共同的指向点 - 软件工程师,似乎所有复杂的源头就是软件工程师的不合格导致,所以其实一些罪恶的根因是我们自己?其实 软件的复杂性是一个基本特征,而不是偶然如此。
对抗软件复杂度的战争在《人月神话》里,作者把复杂性分为两种:
- 本质复杂性(Essential Complexity),指的是你要解决的问题本身的复杂性,是无法避免的。
- 附属复杂性(Accidental Complexity),是指我们在解决本质问题时,所采用的解决方案而引入的复杂性。在我们现在的系统中,90% 的工作量都是用来解决附属复杂性的。例如选择了 Java,选择了容器,选择了中台等等。
- 我喜欢学习各种能力强大的编程语言,例如具备元编程能力的 Ruby 和 Scala,使用这些能力你可以尽情发挥自己的兴趣和创造力。但是我对在有一定团队规模的生产环境中使用这些编程语言持保留意见,因为除非花很大力气 Review 和控制代码风格,否则很可能 10 个人写出来的代码是 10 种风格,这种复杂度的增长是个灾难。相反,使用 Java 这种不那么灵活的语言,大家写代码的风格就比较难不一致。
- 关键干系人的目标事实上是影响软件复杂度的关键因素。我亲眼见过许多案例,其方案空间中明明放着简单的方案,但因为这个原因,当事人不得不选择复杂的方案,例如:原本方案只需要直接改动系统 A,但由于负责系统 A 的团队并没有解决该问题的动力,其他人不得不绕道去修改系统 B,C,D 来解决该问题。
对于复杂系统,在《失控》《领域驱动设计》的书中的都有描述。其实复杂不过于两个维度,而计算机系统,往往是两者一起形成的复杂系统。
- 量变到质变的复杂,比如一座城市发生一起交通事故和同时发生二十起交通事故,对于交管部门,处理起来更麻烦,问题变复杂。
- 难以预测的复杂,这一类的复杂就如同一道数学题,除非你掌握了它的规律,否则你根本无法求解;
比如做一个电商系统,成本是多少?可能几千块,也可能很多亿。如果你理解这个的答案,那意味着比较理解当前软件编程的复杂性问题。因为软件系统的复杂性会随着规模急剧上升。软件的本质复杂度实际上是问题空间(或者称之为业务)带来的,因此给软件加入越多的功能,那么它就必然会包含越多的本质复杂度。此外,每解决一个问题,就对应了一个方案,而方案的实现必然又引入新的偶然复杂度,例如为了实现支付,需要实现某种新的协议,以对接一个三方的支付系统。本质复杂度是一个方面,毕竟更多用户意味着更多的功能特性,但我们无法忽略这里的偶然复杂度
- 其中最典型的就是分布式系统引入的偶然复杂度(调度系统、负载均衡系统、服务发现,RPC,消息系统、高可用体系)。
- 相比于分布式系统引入的复杂度,团队的扩张更易带来偶然复杂度的急剧增长。如果企业没有严格清晰的人才招聘标准,人员入职后没有严格的技术规范培训,当所有人以不同的风格,不同的长短期目标往代码仓库中提交代码的时候,软件的复杂度就会急剧上升。
- 团队的扩张还会带来另外一个问题,在大规模的团队中,关键干系人的目标事实上是影响软件复杂度的关键因素。我亲眼见过许多案例,其方案空间中明明放着简单的方案,但因为这个原因,当事人不得不选择复杂的方案,例如:原本方案只需要直接改动系统 A,但由于负责系统 A 的团队并没有解决该问题的动力,其他人不得不绕道去修改系统 B,C,D 来解决该问题。
万字长文浅谈三高系统建设方法论和实践整个软件的发展历程是一部软件复杂性对抗史,软件的复杂性分为技术复杂性和业务复杂性,业务复杂性主要是建模和抽象设计,技术复杂性主要是三高(高性能,高并发,高可用)的应对,C端的业务一般以技术复杂性为主,业务复杂性为辅,而B端或者M端的业务通常以业务复杂性为主,技术复杂性为辅。个人理解三高系统的核心在于高性能,系统的性能高,系统的处理速度快,吞吐量自然高,此时系统能够应对高并发的流量;系统的性能高,我们系统的对外承诺的TP99, TP999就比较低,超时等影响可用率的情况自然减少,系统的可用性也会提高,所以三高系统建设的出发点可以从系统的性能如何优化进行建设,高性能篇从系统的读写两个维度谈下系统的性能优化方法论及实践。首先我们要清楚知道影响系统性能的因素有那些,通常有以下三方面的因素:计算(computation),通信(communication),存储(storage)。计算层面:系统本身的计算逻辑复杂,Fullgc;通信层面:依赖的下游耗时比较高;存储层面:大库大表,慢sql,ES集群的数据节点,索引,分片,分片大小设置的不合理;针对这些问题,我们可以从读写两个维度针对性能问题进行优化,下图是我工作中解决性能问题的一些方法。
- 读优化,缓存、并行、批量、读写分离、池化
- 写优化,缓存,异步化,数据分片,任务分片 高可用的建设通常是通过保护系统和冗余的方法来进行容错保证系统的可用性。
- 应用层,限流、熔断、超时重试、隔离、兼容
- 存储层,复制、分区
- 部署层,异地多活、多机房多集群、单元化
如何降低复杂性
降低软件复杂性的一般原则和方法 斯坦福教授、Tcl语言发明者John Ousterhout 的著作《A Philosophy of Software Design》
\[C=\sum_{p}c_pt_p\]子模块的复杂度Cp乘以该模块对应的开发时间权重值tp,累加后得到系统的整体复杂度C。也就是说,即使某个模块非常复杂,如果很少使用或修改,也不会对系统的整体复杂度造成大的影响。子模块的复杂度Cp是一个经验值,它关注几个现象:
- 修改扩散,修改时有连锁反应。
- 认知负担,开发人员需要多长时间来理解功能模块。
- 不可知(Unknown Unknowns),开发人员在接到任务时,不知道从哪里入手。
- 认知成本,是指开发人员需要多少知识才能完成一项任务。clean architecture/clean code
如何从容应对复杂性软件设计的最大目标,就是降低复杂度(complexity),具体就是设计符合业务的构造定律的演进方式,一种可以以最小的开发维护成本, 使业务更快更好的流动发展的方式。软件复杂性来自哪里, 如何解决?
- 不确定性:业务、技术、人员流动的不确定性
- 无序性,如何解决:统一认知(秩序化);系统清晰明了的结构(结构化);业务开发流程化(标准化)
- 规模:业务、人员、组织
导致复杂度增长的几个核心因素,包括业务复杂度的增长,分布式系统规模的增长,团队规模的增长,以及关键干系人目标的因素。这其中,分布式系统引入的偶然复杂度是最容易被消除的。今天的云厂商,包括阿里巴巴,亚马逊,谷歌和微软等,在这方面都具有丰富的经验,并且已经通过多年的积累,把这些经验通过商业产品提供给市场。几乎所有的业务,贴近直接用户价值(也不成熟)都必须是要自己研发和承担复杂度的,而只要做好正确的软件架构,那么就能把远离直接用户价值、有现成商业产品的部分提取出来,直接购买。
组织层面
- 不管你们有多敬业,加多少班,在面对烂系统时,你任然会寸步难行,因为你大部分的精力不是在开发需求,还是在应对混乱。造成这种局面,我们的技术管理者,我们的TL要负有主要责任。说的重一点,是工作上的失职,这种失职主要体现在两个方面,一个是技术不作为,另一个是业务不思考。
- 技术人员的疲于奔命,内因上是由于上面分析的团队技术味道的缺失,外因上主要是PD的乱作为。
软件研发的核心职责之一是关注软件复杂度,通过开放代码、文档,Code Review 等方式让软件复杂度的信息透明,并且让所有在增加/降低复杂度的行为透明,并且持续激励那些消除复杂度的行为。唯有如此,在微观层面的控制复杂度的方法才能得到落实。
介于宏观的技术战略和微观的工程师文化之间,存在着一块重要的决策区域,也对软件复杂度有着关键的影响,我称之为系统架构。在面对需求的时候,缺乏经验的工程师会直接想着在自己熟悉的模块中直接解决,而经验丰富的工程师会先思考一下系统上下文。仔细去分析这一复杂度形成的因素,我发现这既不是技术战略的问题,也不是微观层面工程师生产低质量代码导致,而是有其他更深层次的问题。其中的最核心的因素是,这些子系统在不同时期是归属于不同的团队的,有的甚至是不同部门的,具体来说,当各个部门各个团队目标不一致的时候,而这个系统又不幸地被拆到各个团队,那么就不会有人会对系统整体的复杂度控制负责。当有的团队在负责把这套系统商业化对外输出,有的团队在负责把这套系统从虚拟机模式演进到容器模式,有的团队在负责资源的成本控制,有的团队在思考全局高可用架构,而没有一个全局的架构师从整体控制概念,控制边界的时候,系统就自然而然地腐化成这样的一个状态了。康威定律所揭示的事实,就是软件架构在很大程度上是由组织的结构和协作模式决定的,这实际上已经不再是一个软件技术问题了,而是一个组织管理问题。因此,解决系统架构层面的软件复杂度问题,就必须面对组织管理的挑战。关键问题域是否有唯一的负责人?当不同的团队在同一个问题域重复建设系统的时候,如何整合团队?当已有团队为了自己的生存,不断夸大其负责系统的重要性和特殊性,如何识别这种问题?组织如何给予大家充分的安全感,让工程师愿意为了架构的合理性,放弃自己辛苦耕作的系统模块?
架构设计
所有的软件架构万变不离其宗,都在致力解决软件的复杂性。软件的本质是约束。商品的代码不能写在订单域,数据层的方法不能写在业务层。70年的软件发展,并没有告诉我们应该怎么做,而是教会了我们不该做什么。
我们观察地图,其实除了中国、俄罗斯以外,全世界99%的国家都是小国。分裂才是常态,统一才不正常。所以我们不应该问为什么欧洲是分裂的,而应该问为什么中国是统一的。软件的复杂性是一个基本特征,而不是偶然如此。世间万物都需要额外的能量和秩序来维持自身,所有的事物都在缓慢地分崩离析。没有外部力量的注入事物就会逐渐崩溃,这是世间万物的规律,而非我们哪里做得不对。为软件系统注入的外力就是我们的软件架构,以及我们未来的每一行代码。
阿里研究员:警惕软件复杂度困局软件设计和实现的本质是工程师相互通过“写作”来交流一些包含丰富细节的抽象概念并且不断迭代过程。越是大型系统,越需要简单性。对于真正重要的、长生命周期的软件演进,我们需要做到对于复杂度增量零容忍。
架构这个词似乎蕴含了一种建造和设计的意味。一个摩天大楼无论多么复杂,都是事先可以根据设计出完整详尽的图纸,按图准确施工,保证质量就能建造出来的。然而现实中的大型软件系统,却不是这么建造出来的。软件是长出来的。大型软件设计核心要素是控制复杂度。这一点非常有挑战,根本原因在于软件不是机械活动的组合,不能在事先通过精心的“架构设计”规避复杂度失控的风险:相同的架构图/蓝图,可以长出完完全全不同的软件来。所以说了这么多是要停留在形而上吗?并不是。我们的结论是,软件架构师最重要的工作不是设计软件的结构,而是通过API,团队设计准则和对细节的关注,控制软件复杂度的增长。
软件复杂度从根本上说可以说是一个主观指标,说其主观是因为软件复杂度只有在程序员需要更新、维护、排查问题的时候才有意义。复杂度指的是软件中那些让人理解和修改维护的困难程度。相应的,简单性,就是让理解和维护代码更容易的要素。因此我们将软件的复杂度分解为两个维度,都和人理解与维护软件的成本相关:认知负荷与协同成本
- 认知负荷 cognitive load :理解软件的接口、设计或者实现所需要的心智负担。
- 定义新的概念带来认知负荷,而这种认知负荷与 概念和物理世界的关联程度相关。
- 逻辑符合思维习惯程度:正反逻辑差异,逻辑嵌套和独立原子化组合。继承和组装差异。
- 接口设计不当,比如暴露出去的方法有一些隐含约定,进而导致使用不当。也比如 有多种方式让调用者实现完全相同的功能
- 一个简单的修改需要在多处更新,命名(Naming的难度在于对于模型的深入思考和抽象,而这往往确实是很难的。)
- 协同成本Collaboration cost:团队维护软件时需要在协同上额外付出的成本。
- 增加一个新的特性往往需要多个工程师协同配合,甚至多个团队协同配合
- 测试以及上线需要协调同步。交付给其他团队(包括测试团队)的代码应该包含充分的单元测试,具备良好的封装和接口描述,易于被集成测试的。然而因为 单测不足/模块测试不足,带来的集成阶段的复杂度升高、失败率和返工率的升高,都极大的增加了协同的成本。
真正的工程师一定在意自己的作品:我们的作品就是我们的代码。工匠精神是对每个工程师的要求。
降低软件复杂性的一般原则和方法解决复杂性的一般原则,
- 设计是迭代出来的。 好的设计是日拱一卒的结果,在日常工作中要重视设计和细节的改进。
- 拒绝战术编程。战术编程致力于完成任务,新增加特性或者修改Bug时,能解决问题就好。修改Bug时,也应该抱着设计新系统的心态,完工后让人感觉不到“修补”的痕迹。有一种观点认为,创业公司需要追求业务迭代速度和节省成本,可以容忍糟糕的设计,这是用错误的方法去追求正确的目标。降低开发成本最有效的方式是雇佣优秀的工程师,而不是在设计上做妥协。
- 设计两次。为一个类、模块或者系统的设计提供两套或更多方案,有利于我们找到最佳设计。
- 分层 ==> 专业化分工和代码复用;每一层最多影响两层,也给维护带来了很大的便利。
- 分模块,分模块降低了单模块的复杂性,但是也会引入新的复杂性。深模块和浅模块。
软件工程最大的成本在于维护,我们每一次代码的改动,都应该是对历史代码的一次整理,而非单一的功能堆积。当我们使用一些极端的手段来保持古老而陈腐的软件继续工作时,这是一种苟且。
分模块
Unix操作系统文件I/O是典型的深模块,以Open函数为例,接口接受文件名为参数,返回文件描述符。但是这个接口的背后,是几百行的实现代码,用来处理文件存储、权限控制、并发控制、存储介质等等,这些对用户是不可见的。
与深模块相对的是浅模块(Shallow Module),功能简单,接口复杂。通常情况下,浅模块无助于解决复杂性。因为他们提供的收益(功能)被学习和使用成本抵消了。以Java I/O为例,从I/O中读取对象时,需要同时创建三个对象FileInputStream、BufferedInputStream、ObjectInputStream,其中前两个创建后不会被直接使用,这就给开发人员造成了额外的负担。默认情况下,开发人员无需感知到BufferedInputStream,缓冲功能有助于改善文件I/O性能,是个很有用的特性,可以合并到文件I/O对象里。假如我们想放弃缓冲功能,文件I/O也可以设计成提供对应的定制选项。
业务逻辑和技术细节的分离
应用架构之道:分离业务逻辑和技术细节架构始于建筑,是因为人类发展(原始人自给自足住在树上,也就不需要架构),分工协作的需要,将目标系统按某个原则进行切分,切分的原则,是要便于不同的角色进行并行工作。
作为架构师,我们最重要的价值应该是“化繁为简”。架构师的工作就是要努力训练自己的思维,用它去理解复杂的系统,通过合理的分解和抽象,使那些系统不再那么难懂。
六边形架构、洋葱圈架构、以及COLA架构的核心职责就是要做核心业务逻辑和技术细节的分离和解耦。
业务逻辑抽象
按照Wikipedia上的解释,抽象是指为了某种目的,对一个概念或一种现象包含的信息进行过滤,移除不相关的信息,只保留与某种最终目的相关的信息。例如,一个 皮质的足球 ,我们可以过滤它的质料等信息,得到更一般性的概念,也就是 球 。从另外一个角度看,抽象就是简化事物,抓住事物本质的过程。
OOP可以看作一种在程序中包含各种独立而又互相调用的对象的思想,这与传统的思想刚好相反:传统的程序设计主张将程序看作一系列函数的集合,或者直接就是一系列对电脑下达的指令。
当发现有些东西就是不能归到一个类别中时,我们应该怎么办呢?此时,我们可以通过拔高一个抽象层次的方式,让它们在更高抽象层次上产生逻辑关系。比如,你可以合乎逻辑地将苹果和梨归类概括为水果,也可以将桌子和椅子归类概括为家具。但是怎样才能将苹果和椅子放在同一组中呢?仅仅提高一个抽象层次是不够的,因为上一个抽象层次是水果和家具的范畴。因此,你必须提高到更高的抽象层次,比如将其概括为“商品”。
在程序设计中,也是一样,如果在一个类或者一个函数中涉及过多的内容和概念,我们大脑也会显得不知所措,会觉得很复杂,不能理解。将一个大方法,按照逻辑关系,整理成一组更高层次的小而内聚的子程序的集合,那么整个代码逻辑就会呈现出完全不一样的风貌,显得干净、容易理解的多。
分层是一种常见的根据系统中的角色(职责拆分)和组织代码单元的常规实践。
李云:我们在编程时,其实包含无意识的两大步骤:第一步完成的是根本任务,即构思好概念和概念之间的关系,这一步我称之为“软件设计”;第二步完成的是次要任务,即在满足时间冗余度和空间冗余度的情形下,将概念用编程语言表达出来,这一步就是编码工作。布鲁克斯指出,过去软件生产效率的巨大进步得益于在次要任务上投入了巨大的努力。比如,新的编程语言、更快的处理器等。然而,除非次要任务占整个软件开发活动的 90%,否则即便将次要任务所花费的时间缩减到零,也不能带来软件生产效率数量级的提高。这就是“没有银弹”四个字的核心所指。即便进入 21 世纪的今天,这一论断依然成立。软件行业在根本任务的生产效率上,并没有取得次要任务那样质的进步。回到“程序易于理解的核心是什么”这个问题上,我对好程序的第二层理解是:好程序有着清晰、颗粒度合适、连贯且一致的概念。强调概念质量的背后,表达的是对程序的软件设计质量的高要求,这样的程序才易于理解。概念能力是指个体理解、分析和处理复杂概念和问题的能力。这种能力通常涉及从现象中抽象出关键信息,形成整合的观念和理论,进而能够对复杂的情境进行有效的理解和处理。你知道吗?概念能力是人类应对复杂度的一种独特能力,帮助我们理解并解决大量个例问题。新概念的提出一开始会带来更高的概念成本,需要我们花一定的时间去学习和掌握。可一旦概念被普及,就能提升沟通的效率。那我们为什么最终会喜欢新的概念呢?因为概念将大大降低大脑需要处理的信息量,降低生物能耗,是进化带给我们的一种生存能力使然。在工作中,你可能听到过“分而治之”这个词,讲的是对一个复杂的软件系统,用以大化小、拼积木的方法来实现。当你知道了程序中概念的核心作用后,就可以理解为,复杂的软件系统是通过概念的切分与塑造来实现的。PS:一个设计的概念质量如何,低level的抽象是看到一堆细节。面向概念编程。
- 第一层理解是:让人易于理解的程序才是好程序。
- 第二层理解是:好程序有着清晰、颗粒度合适、连贯且一致的概念。
- 好程序的第三层理解:好程序是让人容易修改的。对于代码来说,我们怕的不是它不完善,而是改不动。 “程序的性能、算法、内存开销等要素,为何没有作为好程序的评价指标?”好程序是通过优化而演进出来的。因此,只要程序易懂又好改,其他的问题都不是问题。
布鲁克斯指出软件开发活动由根本任务和次要任务组成。进一步我总结为,根本任务是“软件设计”,次要任务是“编码”,所以我们会有“软件开发 =k × 软件设计 + 编码”这个公式,其中 k 是远大于 1 的系数,软件设计能力才是行业最稀缺的能力。换句话说,软件设计能力是工程师的核心竞争壁垒。软件开发活动的关键是要解决现实问题,这就需要我们根据纷繁复杂的现象和需求,通过抽象和洞察化繁为简。先来说说抽象能力。抽象动作的背后,需要咱有良好的概念能力才行。软件设计能力之所以能成为工程师的竞争壁垒,是因为背后所需的概念能力需要花很多时间来培养,无法速成。
- 对复杂概念和系统的深刻理解需要时间。我们需要在实践中反复应用这些概念,从而深化理解。
- 经验积累需要时间。概念能力往往与个人的经验紧密相关,通过经历不同的项目、挑战和失败,我们才能更好地理解和应用抽象概念。
- 思维模式的转变需要时间。发展概念能力,通常需要我们改变和扩展现有的思维模式,这种转变并非一蹴而就,而是随着时间和经验的积累逐渐完成的。 说完抽象能力,接下来咱来说一说洞察能力,或简称为洞察力。洞察力通常指对现象或问题深刻而敏锐的理解,它涉及快速准确地捕捉到核心问题或隐藏的模式,并能理解背后的深层含义。洞察力是概念能力的一部分,但更侧重于直觉。就概念能力和洞察力,我简单总结一下。概念能力是一种更广泛的认知能力,涵盖了从现象抽象和形成概念的能力;而洞察力则是在此基础上,对特定情境的深入理解和感知。两者共同作用,使个体能够有效地理解和处理复杂的问题,最后做出有洞察力的行为,比如决策、表达观点等。为了让你对洞察力有更具体的认识,就软件设计我想和你分享我的两个洞察。
- 第一个洞察是,越复杂的软件,对工程师软件设计能力的要求就越高。因此,我鼓励你抓住机会,参与更大规模软件项目的开发工作。或者,在已参与的软件项目中,承担更大范围的软件维护和迭代演进责任。这些机会都能很好地锻炼咱驾驭复杂软件的能力,锻炼个体的软件设计能力。
- 第二个洞察是,规模越大的软件,软件设计质量对整个软件产品质量的贡献也越大。
写复杂业务逻辑的方法论
很多人认为做业务开发显得没那么有挑战性,但其实正好相反。最难解决的bug是无法重现的bug,最难处理的问题域是不确定性的问题域。业务往往是最复杂的,面向不确定性设计才是最复杂的设计。软件工程学科最难的事情是抽象,因为它没有标准、没有方法、甚至没有对错。
过程分解和对象建模相结合
”能力下沉“
一般来说实践DDD有两个过程:
- 套概念阶段:了解了一些DDD的概念,然后在代码中“使用”Aggregation Root,Bounded Context,Repository等等这些概念。更进一步,也会使用一定的分层策略。然而这种做法一般对复杂度的治理并没有多大作用。
- 融会贯通阶段:术语已经不再重要,理解DDD的本质是统一语言、边界划分和面向对象分析的方法。
指导下沉有两个关键指标:
- 复用性,复用性是告诉我们When(什么时候该下沉了),即有重复代码的时候
- 内聚性,内聚性是告诉我们How(要下沉到哪里),功能有没有内聚到恰当的实体上,有没有放到合适的层次上(因为Domain层的能力也是有两个层次的,一个是Domain Service这是相对比较粗的粒度,另一个是Domain的Model这个是最细粒度的复用)。
矩阵思维
面对复杂业务,if-else coder 如何升级?业务的差异性是if-else的根源。要如何消除这些讨厌的if-else呢?我们可以考虑以下两种方式:
- 多态扩展:利用面向对象的多态特性,实现代码的复用和扩展。
- 代码分离:对不同的场景,使用不同的流程代码实现。这样很清晰,但是可维护性不好。
结构化思维有用、很有用、非常有用,只是它更多关注的是单向维度的事情。比如我要拆解业务流程,我要分解老板给我的工作安排,我要梳理测试用例,都是单向维度的。而复杂性,通常不仅仅是一个维度上的复杂,而是在多个维度上的交叉复杂性。当问题涉及的要素比较多,彼此关联关系很复杂的时候,两个维度肯定会比一个维度要来的清晰,这也是为什么说矩阵思维是比结构化思维更高层次的思维方式。
除了工作,生活中也到处可见多维思考的重要性。比如,我们说浪费可耻,应该把盘子舔的很干净,岂不知加上时间维度之后,你当前的舔盘,后面可能要耗费更多的资源和精力去减肥,反而会造成更大的浪费。我们说代码写的丑陋,是因为要“快速”支撑业务,加上时间维度之后,这种临时的妥协,换来的是意想不到的bug,线上故障,以及无止尽的996。简单的思考是“点”状的,比如舔盘、代码堆砌就是当下的“点”;好一点的思考是“线”状,加上时间线之后,不难看出“点”是有问题的;再全面一些的思考是“面”(二维);更体系化的思考是“体”(三维);比如,RFM模型就是一个很不错的三维模型。可惜的是,在表达上,我们人类只能在二维的空间里去模拟三维,否则四维可能会更加有用。
我们在做矩阵分析的时候,纵轴可以选择使用业务场景,横轴是备选维度,可以是受场景影响的业务流程(如文章中的商品流程矩阵图),也可以是受场景影响的业务属性(如文章中的订单组成要素矩阵图),或者任何其它不同性质的“东西”。
“业务理解–>领域建模–>流程分解–>多维分析”是体力,是因为实现它们就像是在做填空题,只要你愿意花时间,再复杂的业务都可以按部就班的清晰起来。PS: 有了思维模型,就是在做填空题
自上而下的结构化分解+自下而上的面向对象分析
- 说实话,能想到分而治之的工程师,已经做的不错了,至少比没有分治思维要好很多。我也见过复杂程度相当的业务,连分解都没有,就是一堆方法和类的堆砌。使用过程分解之后的代码,比以前的代码更清晰、更容易维护了。不过,还有两个问题值得我们去关注一下:
- 领域知识被割裂肢解。过程化拆解导致没有一个聚合领域知识的地方。每个 Use Case 的代码只关心自己的处理流程,知识没有沉淀。相同的业务逻辑会在多个 Use Case 中被重复实现,导致代码重复度高,即使有复用,最多也就是抽取一个 util,代码对业务语义的表达能力很弱,从而影响代码的可读性和可理解性。
- 代码的业务表达能力缺失。试想下,在过程式的代码中,所做的事情无外乎就是取数据 – 做计算 – 存数据,在这种情况下,要如何通过代码显性化的表达我们的业务呢?说实话,很难做到,因为我们缺失了模型,以及模型之间的关系。脱离模型的业务表达,是缺少韵律和灵魂的。
-
在系统中引入了更加贴近现实的对象模型(CombineBackOffer 继承 BackOffer),对象模型更加清晰的还原了业务语义,多态可以消除我们代码中的大部分的 if-else。
在现实业务中,很多的功能都是用例特有的(Use case specific)的,如果“盲目”的使用 Domain 收拢业务并不见得能带来多大的益处。相反,这种收拢会导致 Domain 层的膨胀过厚,不够纯粹,反而会影响复用性和表达能力。
我们承认模型不是一次性设计出来的,而是迭代演化出来的。不强求一次就能设计出 Domain 的能力,也不需要强制要求把所有的业务功能都放到 Domain 层,而是采用实用主义的态度,即只对那些需要在多个场景中需要被复用的能力进行抽象下沉,而不需要复用的,就暂时放在 App 层的 Use Case 里就好了。PS:Use Case 是《架构整洁之道》里面的术语,简单理解就是响应一个 Request 的处理过程。
复用性是告诉我们 When(什么时候该下沉了),即有重复代码的时候。内聚性是告诉我们 How(要下沉到哪里),功能有没有内聚到恰当的实体上(甚至要新建一个实体 来容纳很多类似于 StringUtils.equals("xx","xx")
的逻辑),有没有放到合适的层次上(因为 Domain 层的能力也是有两个层次的,一个是 Domain Service 这是相对比较粗的粒度,另一个是 Domain 的 Model 这个是最细粒度的复用)。
有过程分解要好于没有分解,过程分解+对象模型要好于仅仅是过程分解。做不好业务开发的,也做不好技术底层开发,反之亦然。业务开发一点都不简单,只是我们很多人把它做‘简单’了
错误的应对方式
对抗软件复杂度的战争面对效率地不断下降,研发团队的管理者必须做点什么。不幸的是,很多管理者并不明白效率的降低是由软件复杂度的上升造成的,更没有冷静地去思考复杂度蔓延直至爆炸的根因是什么,于是我们看到许多管理者其肤浅的应对方式收效甚微,甚至起到了反作用。
- 最常见的错误方式是设置一个不可更改的 Deadline,用来倒逼研发团队交付功能。但无数经验告诉我们,软件研发就是在质量、范围和时间这个三角中求取权衡。研发团队短期可以通过加班,牺牲假期等手段来争取一些时间(长期加班实际有百害无一利),但如果这个时间限制过于苛刻,那必然就要牺牲需求范围和软件质量。当需求范围不可缩减的时候,唯一可以被牺牲的就只有质量了,这实际就意味着在很短的时间内往系统中倾泻大量的偶然复杂度。
- 另一种做法是用“更先进”的技术去替换现有系统的技术,例如用 Java 的微服务体系技术去替换 PHP + Golang 体系的技术;或者用支撑过成功商业产品的中台类技术去替换原来的微服务体系技术;或者简单到用云产品去替换自建的开源服务。这些做法背后的基本逻辑是,“更先进”的技术在成功的商业场景中被验证过,因此可以被直接拿来解决现有的问题。但在现实情况下,决策者往往忽略了当前的问题是否是“更先进”的技术可以解决的问题。如果现有的系统服务的用户在迅速增长,Scalablity 面临了严重的困境,那么这个答案是肯定的;如果现有的系统的稳定性堪忧,经常不可用且严重影响了用户体验,那么这个答案是肯定的。但是,如果现有的软件系统面临着研发效率下降问题,那么“更先进”的技术不仅帮不了什么忙,还会因为新老技术的切换给系统增加偶然复杂度。
很多人认为做业务开发显得没那么有挑战性,但其实正好相反。最难解决的 bug 是无法重现的 bug,最难处理的问题域是不确定性的问题域。业务往往是最复杂的,面向不确定性设计才是最复杂的设计。软件工程学科最难的事情是抽象,因为它没有标准、没有方法、甚至没有对错。如何在软件固有的复杂性上找到一条既不过度也不缺失的路,是软件工程师的终身课题,或许永远也无法达到,或许我们已经在路上了。
《能力陷阱》
发展人际关系让你觉得卑鄙,虚伪?当我们把人际关系定义为本质上是为了实现自我利益时,甚至有些肮脏时,会限制我们的领导力,限制我们的发展前进,限制我们扩展视野,会阻碍我们了解新想法、发展自己其他方面才能的机会。
Be true to yourself or Shape-shifters (”坚持做自己”,还是”随机应变“)? 对于那些“坚持做自己”的人,作者说你要先知道那个You的定义是什么,是过去的“你”,先现在的“你”,还是未来的“你”呢?是那个固步自封,不敢改变的你,还是那个越来越圆融,知道如何应对变化,可以随机应变的你呢? 所以,坚持做自己很多时候也是自己欺骗自己,不敢跳出舒适圈,不敢做出改变的谎言。Shape-shifters(随机应变者)是指那些很自然可以适应新环境的人,他们并不会产生一种觉得自己很虚假的内疚感。“随机应变者”有一个核心的自我价值观和目标,他们不担心转变自己会对自己的信仰造成影响。
时间是有限的,越是在最忙的时候,越需要空出一些时间来思考做这些事情的原因和目的,不能一直闷着头做。时间安排好,并不是一件特别困难的事。领导者,应该把注意点放在一些非常重要的事情上。然后,其它的时间都要用来自我提升,而不是那些“没有回报的事情”。就是要多花时间在自我提升(学习,写作,分享)上,多花时间对外产生连接,拓展人际网络,扩大影响力,吸引更多人才,从而为团队和组织带来更大贡献