简介
本文主要来自对阿里巴巴高级技术专家张建飞 公众号文章《从码农到工匠》的梳理和总结。作为技术人员,我们不能忘记我们技术人的首要技术使命是治理软件复杂度。
关于原则和方法论,既不必刻意拔高,也不要嗤之以鼻。指导实践的不是更多的实践,而是实践后的总结和思考。
如何定义复杂性
在《人月神话》里,作者把复杂性分为两种:
- 本质复杂性(Essential Complexity),指的是你要解决的问题本身的复杂性,是无法避免的。
- 附属复杂性(Accidental Complexity),是指我们在解决本质问题时,所采用的解决方案而引入的复杂性。在我们现在的系统中,90% 的工作量都是用来解决附属复杂性的。
比如做一个电商系统,成本是多少?可能几千块,也可能很多亿。如果你理解这个的答案,那意味着比较理解当前软件编程的复杂性问题。因为软件系统的复杂性会随着规模急剧上升。
降低软件复杂性的一般原则和方法 斯坦福教授、Tcl语言发明者John Ousterhout 的著作《A Philosophy of Software Design》
\[C=\sum_{p}c_pt_p\]子模块的复杂度Cp乘以该模块对应的开发时间权重值tp,累加后得到系统的整体复杂度C。也就是说,即使某个模块非常复杂,如果很少使用或修改,也不会对系统的整体复杂度造成大的影响。子模块的复杂度Cp是一个经验值,它关注几个现象:
- 修改扩散,修改时有连锁反应。
- 认知负担,开发人员需要多长时间来理解功能模块。
- 不可知(Unknown Unknowns),开发人员在接到任务时,不知道从哪里入手。
组织层面
- 不管你们有多敬业,加多少班,在面对烂系统时,你任然会寸步难行,因为你大部分的精力不是在开发需求,还是在应对混乱。造成这种局面,我们的技术管理者,我们的TL要负有主要责任。说的重一点,是工作上的失职,这种失职主要体现在两个方面,一个是技术不作为,另一个是业务不思考。
- 技术人员的疲于奔命,内因上是由于上面分析的团队技术味道的缺失,外因上主要是PD的乱作为。
架构设计
阿里研究员:警惕软件复杂度困局软件设计和实现的本质是工程师相互通过“写作”来交流一些包含丰富细节的抽象概念并且不断迭代过程。越是大型系统,越需要简单性。对于真正重要的、长生命周期的软件演进,我们需要做到对于复杂度增量零容忍。
架构这个词似乎蕴含了一种建造和设计的意味。一个摩天大楼无论多么复杂,都是事先可以根据设计出完整详尽的图纸,按图准确施工,保证质量就能建造出来的。然而现实中的大型软件系统,却不是这么建造出来的。软件是长出来的。大型软件设计核心要素是控制复杂度。这一点非常有挑战,根本原因在于软件不是机械活动的组合,不能在事先通过精心的“架构设计”规避复杂度失控的风险:相同的架构图/蓝图,可以长出完完全全不同的软件来。所以说了这么多是要停留在形而上吗?并不是。我们的结论是,软件架构师最重要的工作不是设计软件的结构,而是通过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可以看作一种在程序中包含各种独立而又互相调用的对象的思想,这与传统的思想刚好相反:传统的程序设计主张将程序看作一系列函数的集合,或者直接就是一系列对电脑下达的指令。
当发现有些东西就是不能归到一个类别中时,我们应该怎么办呢?此时,我们可以通过拔高一个抽象层次的方式,让它们在更高抽象层次上产生逻辑关系。比如,你可以合乎逻辑地将苹果和梨归类概括为水果,也可以将桌子和椅子归类概括为家具。但是怎样才能将苹果和椅子放在同一组中呢?仅仅提高一个抽象层次是不够的,因为上一个抽象层次是水果和家具的范畴。因此,你必须提高到更高的抽象层次,比如将其概括为“商品”。
在程序设计中,也是一样,如果在一个类或者一个函数中涉及过多的内容和概念,我们大脑也会显得不知所措,会觉得很复杂,不能理解。将一个大方法,按照逻辑关系,整理成一组更高层次的小而内聚的子程序的集合,那么整个代码逻辑就会呈现出完全不一样的风貌,显得干净、容易理解的多。
分层是一种常见的根据系统中的角色(职责拆分)和组织代码单元的常规实践。
写复杂业务逻辑的方法论
过程分解和对象建模相结合
”能力下沉“
一般来说实践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 这个是最细粒度的复用)。
有过程分解要好于没有分解,过程分解+对象模型要好于仅仅是过程分解。做不好业务开发的,也做不好技术底层开发,反之亦然。业务开发一点都不简单,只是我们很多人把它做‘简单’了
《能力陷阱》
发展人际关系让你觉得卑鄙,虚伪?当我们把人际关系定义为本质上是为了实现自我利益时,甚至有些肮脏时,会限制我们的领导力,限制我们的发展前进,限制我们扩展视野,会阻碍我们了解新想法、发展自己其他方面才能的机会。
Be true to yourself or Shape-shifters (”坚持做自己”,还是”随机应变“)? 对于那些“坚持做自己”的人,作者说你要先知道那个You的定义是什么,是过去的“你”,先现在的“你”,还是未来的“你”呢?是那个固步自封,不敢改变的你,还是那个越来越圆融,知道如何应对变化,可以随机应变的你呢? 所以,坚持做自己很多时候也是自己欺骗自己,不敢跳出舒适圈,不敢做出改变的谎言。Shape-shifters(随机应变者)是指那些很自然可以适应新环境的人,他们并不会产生一种觉得自己很虚假的内疚感。“随机应变者”有一个核心的自我价值观和目标,他们不担心转变自己会对自己的信仰造成影响。
时间是有限的,越是在最忙的时候,越需要空出一些时间来思考做这些事情的原因和目的,不能一直闷着头做。时间安排好,并不是一件特别困难的事。领导者,应该把注意点放在一些非常重要的事情上。然后,其它的时间都要用来自我提升,而不是那些“没有回报的事情”。就是要多花时间在自我提升(学习,写作,分享)上,多花时间对外产生连接,拓展人际网络,扩大影响力,吸引更多人才,从而为团队和组织带来更大贡献