虽然镇楼的漫画有点反讽架构设计的意思,但是本文还是主要记录上半年重点在啃的一本书——《架构整洁之道》。Bob大叔的整洁三件套大概是值得每个软件开发从业者都不断去深入学习的一个系列了,所谓“不断”,就是指三年读一遍、五年读一遍、十年再读一遍,估计不同的时间点读到的体会都不一样。经典大概就是这个意思了吧。
内容结构
先从内容结构上看,整本书的逻辑大概是这样的:
- 概述部分主要说明架构的重要性。因为架构的优化对产品功能本身来说从外面看没什么影响,所以第一部分处理介绍是什么之外,主要是鼓励工程师跟业务方作斗争,争取到架构重构的迭代时间
- 第2到5章是递进式的理论讲述。
- 编程范式的部分。大概是从最基层的语言层面来解释一些最基础的核心原则
- 设计原则。向下一层派生出基础的设计原则,以面向对象为例的话,类与类之间的关系就开始需要参考这些原则了。我认为设计模式的来源,也正是这些设计原则
- 组件构建原则。软件规模再具化到组件的级别,同样有这个层级的原则。而在论述过程中也会发现,组件的构建原则跟它的上一个层级——设计原则——是有一定的映射关系的
- 前面三个部分的篇幅基本一致,有了前面的铺垫,第五章重点讲述了软件架构的部分,篇幅大概是前面几章的总和还多。在这一部分的正中,提到了整洁架构;同时出现最多的关键词是【边界】
- 最后一部分是具体的实例讲解
所以整体上大概是讲:为啥要做架构设计、递进式将整洁架构是啥、举个案例告诉你怎么做。呃,这么一看居然暗合了why、what和how的结构,希望不算是生搬硬套。
重新理解软件规则
不得不说这本书帮助我重新梳理了对软件研发各个层级的理论认知,并且它的理论抽象度很高。举个例子,编程范式这种东西30页描述了结构化、面向对象和函数式这三种,其实每一个方向都够出一本书了。这三十页的内容,配合一本叫《七周七语言 理解多种编程范型》的书搭配来看,倒是可以相互印证,提高理解。随便摘几句,感觉每一句都值得深入的思考:
结构化编程对程序控制权的直接转移进行了规范
面向对象编程对程序控制权的间接转移进行了规范
函数式编程对程序中的赋值进行了限制和规范
goto语句,函数指针和赋值语句
多态是跨越架构边界的手段,函数式规范和限制数据存放位置与访问权限的手段,结构化是各模块算法实现的基础
与软件架构的三大关注点不谋而合,功能性、组件独立性以及数据管理
同样的道理,一些基础的设计原则在不同层次的展现,也同样刷新了我对于软件设计的结构化认知。我觉得目前的水平还不足以准确的描述出这些认知,这里只留一些摘记。
设计原则
- SRP 单一职责原则。任何一个软件模块都应该只对某一类行为者负责。强调低耦合。主要讨论的是函数和类之间的关系,主要在两个层面,在组件层面,称为共同闭包原则;在软件架构层面,用于奠定架构边界变更的轴心
- OCP 开闭原则。良好的软件应该易于扩展,同时抗拒修改。好的架构师会努力将旧代码的修改量降至最小,一般会先将满足不同需求的代码分组(SRP);再调整分组之间的依赖关系(DIP)。该原则让系统易于拓展,并限制其修改所带来的影响范围。
- LSP 里氏替换原则。认为接口的类之间具有可替换性,是指导接口与其实现方式的设计原则。正例:个人证书和企业证书,系统的计费方式;反例:长方形和正方形
- ISP 接口隔离原则。任何层次的软件设计如果依赖不需要的东西,都是有害的。
- DIP 依赖反转原则。在代码中多使用抽象接口,避免使用多变的具体实现类;不要在具体实现类上创建衍生类;不要覆盖包含具体实现的函数;避免在代码中写入与具体任何实现相关的名字,或其他容易变动的事物的名字。总之,少用继承。抽象层与具体实现层之间应该存在一个架构边界,控制流跨越该边界的方向与源代码依赖关系跨越该边界的方向必然相反,即所谓依赖反转
组件构建基本原则
- REP:复用/发布原则。软件复用的最小粒度应等同于其发布的最小粒度。一方面组件中的类与模块应该有共同的主题和大方向;另外它们会共享相同的版本号和版本追踪。
- CCP:共同闭包原则。将会同时修改、并且为相同目的修改的类放到同一个组件中,反之,不会同时修改和不会为了相同目的修改的类放到不同组件中。CCP可以认为是SRP的组件版。
- CRP:共同复用原则。不是紧密相关联的类不要放到一个组件中。可以理解为是ISP的普适版,这两条规则可以统一认为是:不要依赖不需要用到的东西。
组件耦合原则
- ADP:无依赖环原则。在组件依赖结构图中,不应该出现环式的依赖关系,因为环式依赖会影响可维护性和可测试性。可以根据DIP优化掉。
- SDP:稳定依赖原则。依赖关系必须要指向更稳定的方向。什么是稳定的组件,让很多其他组件依赖于它,它就应该是稳定的。稳定性指标计算方法:fan-in:入向依赖的组件数量、fan-out:出向依赖的组件数量、不稳定性I=out/(in+out)。不稳定性为1意味着该组件最不稳定。本原则即一个组件的I指标一定是大于它所依赖的组件的I指标。
- SAP:稳定抽象原则。一个组件的抽象化程度应与其稳定性保持一致。要求稳定的组件同时应该是抽象的,这样它的稳定性就不会影响到拓展性;另一方面,不稳定的组件应当包含具体的实现代码,这样它的不稳定性就可以通过具体的代码被轻易修改。SAP与SDP就是组件层级的DIP,因为SDP要求让依赖关系指向更稳定的方向;SAP告诉我们稳定性本身就隐含了对抽象化的要求。即依赖关系应该指向更抽象的方向。抽象化程度计算方式:A=组件中抽象类和接口的数量/组件中所有类的数量。
- 以抽象度A为纵坐标,稳定度I为横坐标 通过区域分析系统的架构健康程度
组件分析的量化
从组件分析的一些原则中,我看到了一些可以量化和工具化的东西 —— 组件耦合原则。比如依赖环的检查、稳定性和抽象性的关系分析。这意味着整个工程的架构健康程度,在依赖关系这个层级上,是可以去做有指标的持续性优化的。
对于组件化的Android工程来说,可以通过编译过程中的产物,从字节码的维度分析出各个组件的内部成分和组件之间的依赖关系。把这些数据记录下来之后就可以做二次的架构健康度的分析,这样算来也可以说这是代码静态质量分析的一种方式。
恰好团队中有类似分析某个组件api变更导致其他依赖方crash这种比较常见的情况的工具,原理相似。借助其中间产物的数据,做了二次分析。确实可以做到类似的分析产出,此外还有附属产物——可以通过这些中间数据产出单组件的依赖关系图,使用plantUML来描述既方便又好用,拓展开来看,似乎也可以生产整个工程视角的组件依赖关系图。当APP完成组件化并且经历业务爆发、组件数量上升到一定程度之后,很难从全局角度看到依赖上的问题,这种分析结果在一定程度上可以指明一些优化方向。
整洁架构
我理解到整洁架构的核心是定义好哪些东西是稳定的、哪些东西是不稳定的,然后使用各种设计技巧调整这两个方向的业务块的方向。同时利用面向接口的思路,让不稳定的部分都是可替换、可拓展的。
这个思考模式对于做惯了简单的业务层-中间层-底层这种分层的客户端APP研发而言,还是具有一定的冲击性的。因为对于整洁架构中认为不稳定的部分,比如设备依赖——对于APP而言可能是Android framework层——在原有的分层设计中的位置可能是在最底层。从简单分层到洋葱型,还是需要一定的持续思考和实践,才能够融会贯通。所以一些博客上讲clean架构的学习成本比较高,可能也是有道理的。
所以架构设计越优雅,似乎整体就会显得越复杂。不过这种复杂可能会换来更高的可拓展性和可测试性,复杂的结构似乎会降低整体的可读性,但是对单点而言可维护性也会提升。从ROI的角度来看,还是要看业务是否复杂到要做这么高阶的架构设计。因为对大多数APP端而言,应用在业务上都会做的比较薄,大部分逻辑和配置都是云端决定的,比如出于动态更新、可降级等方面的考虑。那么也就决定了大部分APP研发可能根本触及不到复杂的架构设计,跟随Google爸爸的脚步从MVP到MVVM基本也就够用了。但是业务到了一定的量级,或者整个业务模型不是简单的客户端-云端模型,而是iot这种三端或者四端的模型,同时又需要产出整体的PAAS方案,那么就不得不向更复杂的架构来靠拢了——clean似乎是一种可行的思路。
小结
对于这种传道的书,还是需要花大力气去精读的。并且如果是处理复杂业务的、需要较大规模团队协作的、有很多研发痛点的同学,更加需要读一读这本书,或许可以找到一些思路、来解决实际的研发问题。