《架构整洁之道》读书笔记
1. 设计与架构
软件架构的终极目标是,用最小的人力成本来满足构建和维护该系统的需求。
一个软件架构的优劣,可以用它满足用户需求所需要的成本来衡量。如果该成本很低,且在系统的生命周期内始终很低,那么这个系统的设计就是优良的。反之,就是不好的设计。
胡乱编写代码的工作速度,其实比循规蹈矩更慢。要想跑得快,先要跑得稳。
2. 两个价值维度
- 行为价值:程序按照需求文档要求的方式工作
- 架构价值:软件的“软”,即软件的灵活性
艾森豪威尔矩阵
重要且紧急 | 重要不紧急 |
不重要但紧急 | 不重要且不紧急 |
- 紧急的事情往往没那么重要,而重要的事情似乎永远也排不上优先级。
- 第一个价值维度:系统行为,是紧急的,但是并不总是特别重要。
- 第二个价值维度:系统架构,是重要的,但是并不总是特别紧急。
- 应有的排序:
- 重要且紧急
- 重要不紧急
- 不重要但紧急
- 不重要且不紧急
- 常犯的错误:将第三优先级的事情提到第一优先级去做。导致重要的事情被忽略。
- 平衡系统架构的重要性与功能的紧急程度,是软件研发人员自己的职责。
建议:为好的软件架构而持续斗争
研发团队必须从公司长远利益出发,与其他部门抗争,公司内部的抗争本来就是无止境的。
软件的可维护性需要由你来保护,这是你的职责,公司雇你的很大一部分原因就是需要有人来做这件事。
3. 编程范式
编程范式指的是程序的编写模式。一共只有三种编程范式,而且未来几乎不可能再出现新的(理由是,编程范式都是增加限制,Bob大叔的理解)。
一本谈软件架构的书,为什么要设计编程范式呢?Bob大叔如是说:
多态是我们跨越架构边界的手段,函数式编程是我们规范和限制数据存放位置与访问权限的手段,结构化编程则是各模块的算法实现的基础。
这和软件架构的三大关注重点不谋而合:功能性、组件独立性、数据管理。
结构化编程
- 可推导性:可以用代码将一些已证明可用的结构串联起来,只要证明额外的代码是正确的,就可以推导出整个程序的正确性
- goto语句的某些用法会导致某个模块无法被递归拆分成更小的、可证明的单元。
- 如果某个结论经过一定的努力无法证伪,则认为它在当下是足够正确的。
- 测试只能展示bug的存在,并不能证明不存在bug。
软件架构师需要定义可方便证伪(测试)的模块、组件及服务。为了达到这个目的,要将类似结构化编程的限制方法应用在更高的层面上。
面向对象编程
以多态为手段来对源代码中的依赖关系进行控制的能力。这种能力让软件架构师可以构建出某种插件式架构,让高层策略组件与底层实现组件相分离。
函数式编程
架构设计良好的程序,应该将状态修改的部分与不需要修改状态的部分隔离成单独的组件,然后用合适的机制来保护可比量。
4. 设计原则
SRP:单一职责原则
人们对单一指责原则的理解,是逐步发展的
- 每个模块都应该只做一件事。
- 任何一个软件模块都应该有且仅有一个被修改的原因。
- 任何一个软件模块都应该只对某一类行为者负责。
OCP:开闭原则
设计良好的计算机软件系统应该易于扩展,同时抗拒修改。换句话说,在不需要修改的前提下就可以轻易被扩展。
一个好的软件架构设计师会努力将旧代码的修改需求量降至最小,甚至为0。如何做到呢?可先将满足不同需求的代码分组(即SRP),然后再来调整这些分组之间的依赖关系(即DIP)。
LSP:里氏替换原则
任何基类可以出现的地方,子类一定可以出现。
在架构上的意义,可以理解为插件式的架构,插件之间、子服务之间的可替换性。
ISP:接口隔离原则
任何层次的软件设计如果依赖了它并不需要的东西,就会带来意料之外的麻烦。
DIP:依赖反转原则
如果想要设计一个灵活的系统,在源代码层次的依赖关系中就该多引用抽象类型,而非具体实现。
相对而言,接口比实现更稳定。如果想要在软件架构设计上追求稳定,就必须多使用稳定的抽象接口,少依赖多变的具体实现。
- 应在代码中多使用抽象接口,尽量避免使用那些多变的具体实现类。
- 不要在具体实现类上创建衍生类
- 在静态类型的编程语言中,继承关系是所有源代码依赖关系中最强的、最难被修改的,所以应该格外小心。
- 不要覆盖(override)包含具体实现的函数
- 应避免在代码中写入与任何具体实现相关的名字,或者是其他容易变动的事物的名字。
5. 组件构建原则
组件聚合
- REP:复用/发布等同原则
- 软件复用的最小粒度应等同于其发布的最小粒度。
- 一个组件内的类和模块之间,应该有一个共同的主题或大方向。
- 一个组件内的类和模块还应该可以同时发布。
- CCP:共同闭包原则
- 单一职责原则在组件层面上的再度阐释。
- 要将所有可能被一起修改的类集中在一处。
- 将由于相同原因而修改,且需要同时修改的东西放在一起。
- CRP:共同复用原则
- 接口隔离原则的一个普适版本。
- 不要强迫一个组件的用户依赖他们不需要的东西。
- 将经常共同复用的类和模块放在同一个组件中。
- 组件聚合原则张力图(TODO:再明确一些,原书第96页)
组件耦合
- 无依赖环原则
- 必须控制好组件之间的依赖结构,绝对不允许该结构中存在循环依赖关系。
- 有向无环图(Directed Acyclic Graph, DAG)
- 打破循环依赖的方法
- 应用依赖反转原则
- 创建一个新组件,让二者都依赖于它
- 自上而下的设计
- 组件结构图是不可能自上而下被设计出来的。它必须随着软件系统的变化而变化、扩张,而不可能在系统构建的最初就完美设计出来。
- 组件结构图并不是用来描述应用程序功能的,它更像是应用程序在构建性与维护性方面的一张地图。
- 组件结构图的一个重要目标是指导如何隔离频繁的变更。
- 稳定依赖原则
- 依赖关系必须要指向更稳定的方向。
- 稳定抽象原则
- 一个组件的抽象化程度应该与其稳定性保持一致。
6. 整洁架构
外层圆代表机制,内层元代表策略。
源码中的依赖关系必须只指向同心圆的内层,即由低层机制指向高层策略。
- 业务实体
- 整个系统的关键业务逻辑
- 最通用、最高层的业务逻辑
- 用例
- 特定场景下的业务逻辑
- 封装并实现了整个系统的所有用例
- 接口适配层
- 通常是一组数据转换器
- 负责将数据从对用例和业务实体而言最方便操作的格式,转换成外部系统最方便操作的格式。
- 从该层往内的同心圆中,其代码就不应该依赖任何数据库了。
- 框架与驱动程序
- 包含了所有的实现细节。
跨越边界的数据结构一般应是简单的(谦卑对象)。不要投机取巧地直接传递业务实体或数据库记录对象。
当我们进行跨边界传输时,一定要采用内层最方便使用的形式。
7. 实现细节
- 数据库是实现细节
- Web是实现细节
- 应用程序框架是实现细节
8. 金玉良言
- 走快的唯一方法是先走好。
- 做一个好的软件架构师所需要的自律和专注程度可能会让大部分程序员始料未及。
- 软件系统不应该依赖其不直接使用的组件。
- 程序规模上的墨菲定律
- 程序的规模会一直不断地增长下去,直到将有限的编译和链接时间填满为止。
- 软件架构师自身需要是程序员,并且必须一直坚持做一线程序员。
- 软件架构师应该是能力最强的一群程序员。
- 如果不亲身承受因系统设计而带来的麻烦,就体会不到设计不佳所带来的痛苦,接着就会逐渐迷失正确的设计方向。
- 要在设计中尽可能长时间地保留尽可能多的可选项。
- 系统维护的主要成本集中在“探秘”和“风险”这两件事上:
- 探秘:确定新增功能或被修复问题的最佳位置和最佳方式。
- 风险:当我们进行上述修改时,总是有可能衍生出新的问题。
- 如果在开发高层策略时,有意地让自己摆脱具体细节的纠缠,就可以将与具体实现相关的细节决策推迟或延后。越到后期,我们就有越多的信息来做出合理的决策。
- 一个优秀的软件架构师应该致力于最大化可选项数量。
- 重复在软件行业里一般来说都是坏事。
- 如果有两端看起来重复的代码,它们走的是不同的演进路径,有着不同的变更速率和变更缘由,那么这两段代码就不是真正的重复。
- 要小心避免陷入对任何重复都要立即消除的应激反应模式中。
- 软件架构设计本身就是一门划分边界的艺术。
- 一个系统最消耗人力资源的是什么?答:耦合
- I/O是无关紧要的细节
- 软件开发技术发展的历史,就是一个如何想方设法方便地增加插件,从而构建一个可扩展、可维护的系统架构的故事。
- 线程既不属于架构边界,又不属于部署单元,仅仅是一种管理并调度程序执行的方式。
- 系统架构中最强的边界形式就是服务。
- 架构设计的工作常常需要将组建重排组合成为一个有向无环图。图中的每一个节点代表一个拥有相同层次策略的组件,每一条单向链接代表了一种组件之间的依赖关系。
- 软件系统是一组策略语句的集合。
- 一条策略距离系统的输入/输出越远,它所属的层级就越高。而直接管理输入/输出的策略在系统中的层次是最低的。
- 希望源码中的依赖关系与其数据流向脱钩,而与组件所在的层次挂钩。
- 一定要带着怀疑的态度审视每一个框架。
- 保持对系统用例的关注,避免让框架主导我们的架构设计。
- 应该通过用例对象来调度业务实体对象,确保所有的测试都不需要依赖框架。
- 任何形式的共享数据行为都会导致强耦合。
- 架构设计的任务就是找到高层策略和低层细节之间的架构边界,同时保证这些边界遵守依赖关系规则。
- 横跨型变更(cross-cutting concern)。系统的架构边界事实上并不落在服务之间,而是穿透所有服务,在服务内部以组件的形式存在。
- 必须在服务内部采用遵守依赖关系原则的组件设计方式。
- 服务边界并不能代表系统的架构边界,服务内部的组件边界才是。
- 系统的架构是由系统内部的架构边界、边界之间的依赖关系所定义的,与系统中各组件之间的调用、通信方式无关。
- 软件设计的第一条原则——不要依赖于多变的东西。
- 测试组件是系统架构中最外圈的程序。它们始终是向内依赖的,而且系统中没有其他组件依赖于它们。
- 软件(software)应该是一种使用周期很长的东西,而固件(firmware)则会随着硬件演进而淘汰过时。
- 软件构建过程的三个阶段
- 先让代码工作起来——如果代码不能工作,就不能产生价值
- 然后再试图将它变好——通过对代码进行重构,让我们自己合其他人更好地理解代码,并能按照需求不断地修改代码。
- 最后再试着让它运行得更快——按照性能提升的“需求”来重构代码。
- 随时准备“抛弃一个设计”
- 在实践中学习正确的工作方法,然后再重写一个更好的版本。
- 如果我们在构建代码的时候不够小心,没有小心安排哪些模块之间可以互相依赖,代码很快就非常难以更改了。
- 软件与固件之间的边界被称为硬件抽象层(HAL)
- 类似的还有,操作系统抽象层(OSAL)
- 从系统架构的角度来看,工具通常是无关紧要的。
- 常见的封装方式
- 按层封装,传统的水平分层架构
- 按功能封装,即垂直切分
- 端口和适配器,业务领域代码与具体实现细节隔离的架构
- 按组件封装
- 软件架构的响应式设计:前期设计够用,后期进行大量重构的设计思想。
- 每一项设计决策都必须为未来的变化敞开大门。
- 大部分的软件开发者是没有太多架构意识的,是更有经验的开发者让我了解了架构。