读书笔记:架构整洁之道
架构整洁之道 clean architecture [美]Robert C.Martin
序言
程序员,工程师,架构师
工程师:他们会用工程的方法来编写代码,以便让编程开发更为高效和快速。
架构师:这个世界不存在完美的解决方案,就像CAP理论。基于业务分析给出平衡方案。
观点:软件都为解决一个问题:分离控制和逻辑。
问题:区分简单与简陋,平衡与妥协,迭代与半成品。
物理建筑的结构必须遵守受到重力影响这一自然规律。
讨论软件架构时,特别注意软件项目是具有递归和分形特点的,最终都要由一行一行的代码组成。脱离实现细节,架构设计就无从谈起。
架构图中代表的彩色方块无法完全描述架构,只能是软件架构的一个视图。
第1部分 概述
第1章 设计与架构究竟是什么 3
目标是什么 4
软件架构的终极目标是,用最小的人力成本来满足构建和维护该系统的需求。
乱麻系统的特点:不管他们投入多少时间,救了多少次火,加了多少次班,他们的产出始终上不去。工程师大部分时间消耗在修修补补上,而不是完成实际的新功能。
案例分析 5
本章小结 11
第2章 两个价值维度 12
行为价值 13
架构价值 13
哪个价值维度更重要 14
系统架构,重要的事情占据了列表的前两位
系统行为,紧急的事情只占据了第一和第三位。
业务部门原本就是没有能力评估系统架构的重要程度。平衡系统架构重要性与功能紧急性本来就是研发的职责。
研发为好的软件架构,公司长远利益出发与产品团队做抗争。
艾森豪威尔矩阵 15
为好的软件架构而持续斗争 16
第2部分 从基础构件开始:编程范式
第3章 编程范式总览 21
结构化编程
Dijkstra于1968年提出,还论证了goto跳转损害程序整体结构。主张用 if/else, do/wihile。
结构化编程对程序控制权的直接转移进行了限制和规范。
面向对象编程
1966年,Johan, Kriste论文中归纳出来。注意到函数调用栈(call stack frame)可以挪到堆内存里,这样函数定义的本地变量可以在函数返回后继续存在。这个函数就是类的构造函数,本地变量就是类的属性。用多态限制函数指针使用。
面向对象编程对程序控制权的间接转移进行了限制和规范。
函数式编程
1958年john mccarthy发明LISP语言。核心思想是不可变性:某个符号所对应的值是永远不变的。
函数式编程对程序中的赋值进行了限制和规范。
仅供思考 23
本章小结 24
第4章 结构化编程 25
可推导性
Dijkstra发现goto语句无法被递归拆分成可证明的单元。if/else,do/while可以。
Bohm, Jocopini证明顺序,分支,循环可以构造任何程序。证明了构建可推导模块所需结构集合与构建程序的控制最小集合是等同的。结构化编程就诞生了。
科学理论的特点:可以被证伪,但没法证明。科学方法论不需要证明某条结论正确,只需想法证明它是错误的。如果无法证伪,则认为当下正确。
goto是有害的 28
功能性降解拆分 29
形式化证明没有发生 29
科学来救场 29
测试
Dijkstra说过测试只能证明程序错误,但不能证明程序完全正确。
本章小结 31
第5章 面向对象编程 32
什么是面向对象?
封装
C的头文件可以不包含结构体内容,但c++必须要类的具体定义,反而破坏了封装。
继承
简而言之,继承的主要作用是让我们可以对外部定义某一组变量与函数进行覆盖。
C实现
struct NamedPoint* origin;
(struct Point*)origin;// 强制类型转换
多态
UNIX强制要求IO设备提供open, close, read, write, seek 5个标准函数。
多态不过就是函数指针的一种应用。
本章小结 44
第6章 函数式编程 45
整数平方 46
不可变性与软件架构 47
可变性的隔离 48
事件溯源 49
本章小结 51
第3部分 设计原则
SOLID原则的主要作用就是告诉我们如何将数据和函数组织成为类,以及如何将这些类链接起来成为程序。请注意,这里虽然用到了“类”这个词,但是并不意味着我们将要讨论的设计原则仅仅适用于面向对象编程。
设计原则:
- SRP:单一职责原则。康威定律的一个推论——一个软件系统的最佳结构高度依赖于开发这个系统的组织的内部结构。每个软件模块都有且只有一个需要被改变的理由。
- OCP:开闭原则。如果软件系统想要更容易被改变,那么其设计就必须允许代码来修改系统行为,而非只能修改原来的代码。
- LSP:里氏替换原则。
- ISP:接口隔离原则。软件设计中避免不必要的依赖。
- DIP:依赖反转原则。高层策略性的代码不应该依赖实现底层细节的代码,恰恰相反,那些实现底层细节的代码应该依赖高层策略性的代码。
第7章 SRP:单一职责原则
定义:任何一个软件模块都应该只对某一类行为者负责。
反面案例2:代码合并
避免这种问题产生的方法就是将服务不同行为的代码进行切分。
解决方案 60
本章小结 61
第8章 OCP:开闭原则 62
思想实验 63
依赖方向的控制 67
信息隐藏 67
本章小结 67
第9章 LSP:里氏替换原则 68
继承的使用指导 69
正方形/长方形问题 70
LSP与软件架构 70
违反LSP的案例 71
本章小结 73
第10章 ISP:接口隔离原则 74
ISP与编程语言 76
ISP与软件架构 76
本章小结 77
第11章 DIP:依赖反转原则 78
稳定的抽象层 79
工厂模式
这里控制流跨越架构边界的方向与源代码依赖关系跨越该边界的方向正好相反,源代码依赖方向永远是控制流方向的反转——这就是DIP被称为依赖反转原则的原因。
具体实现组件 82
本章小结 82
第4部分 组件构建原则
第12章 组件
组件是软件的部署单元。
设计良好的组件都应该永远保持可被独立部署的特性,这同时意味着这些组件应该可以被单独开发。
组件发展史 85
重定位技术 88
链接器 88
本章小结 90
第13章 组件聚合
究竟哪些类应该被组合成一个组件呢?
构建组件相关的基本原则:
- REP:复用/发布等同原则
- CCP:共同闭包原则
- CRP:共同复用原则
复用/发布等同原则
定义:软件复用的最小粒度应等同于其发布的最小粒度。
共同闭包原则
定义:我们应该将那些会同时修改,并且为相同目的而修改的类放到同一个组件中。而将不会同时修改,并且不会为了 相同目的而修改的那些类放到不同的组件中。
与SRP原则的相似点:
将由于相同原因而修改,并且需要同时修改的东西放在一起。将由于不同原因而修改,并且不同时修改的东西分开。
共同复用原则
定义:不要强迫一个组件的用户依赖他们不需要的东西。
每当被引用组件发生变更时,引用它的组件一般也需要做出响应的变更。即使它们不需要进行代码级的变更,一般也免不了被重新编译、验证和部署。
与ISP原则的关系:
不要依赖不需要用到的东西。
组件聚合张力图
REP和CCP原则是黏合性原则,它们会让组件变得更大,而CRP原则是排除性原则,它会尽量让组件变小。
一般来说,一个软件项目的重心会从该三角区域的右侧开始,先期主要牺牲的是复用性。然后,随着项目逐渐成熟,其他项目会逐渐开始对其产生依赖,项目重心会逐渐向该三角区域的左侧滑动。
本章小结 97
第14章 组件耦合
无依赖环原则
定义:组件依赖关系图中不应该出现环
如果组件发布了新版本,判断出哪些组件会受影响——只需要按其依赖关系反向追溯即可。
打破循环依赖
- 应用依赖反转原则(DIP)
- 创建一个新的组件。
自上而下的设计 105
稳定依赖原则
定义:依赖关系必须要指向更稳定的方向
稳定性:让软件组件难于修改的一个最直接的办法就是让很多其他组件依赖于它。带有许多入向依赖关系的组件是非常稳定的。如:有三个组件依赖于X,所以X有三个不应该被修改的原因。
稳定抽象原则
定义:一个组件的抽象化程度应该与其稳定性保持一致。
稳定抽象原则(SAP)为组件的稳定性与它的抽象化程度建立了一种关联。一方面,该原则要求稳定的组件同时应该是抽象的,这样它的稳定性就不会影响到扩展性。
本章小结 117
第5部分 软件架构
第15章 什么是软件架构
软件架构师自身需要是程序员,并且必须一直坚持做一线程序员。
真正的麻烦往往并不会在我们运行软件的过程中出现,而是会出现在这个软件系统的开发、部署以及后续的补充开发中。
软件架构的终极目标就是最大化程序员的生产力,同时最小化系统的总运营成本。
开发(Development)
部署(Deployment)
系统的部署成本越高,可用性就越低。因此,实现一键式的轻松部署应该是我们设计软件架构的一个目标。
运行(Operation)
维护(Maintenance)
保持可选项 124
设备无关性 126
垃圾邮件 128
物理地址寻址 129
本章小结 130
第16章 独立性 131
用例 132
运行 133
开发
康威定律:任何一个组织在设计系统时,往往都会复制出一个与该组织内沟通结构相同的系统。
部署 134
保留可选项
一个设计良好的架构应该通过保留可选项的方式,让系统在任何情况下都能方便地做出必要的变更。
按层解耦
从用例的角度来看,架构师的目标是让系统结构支持其所需要的所有用例。但是问题恰恰是我们无法预知全部的用例。
用例的解耦
按照用例来切分系统是非常自然的选择。
解耦的模式
开发的独立性 137
部署的独立性 137
重复 138
再谈解耦模式 139
- 源码层次:系统所有的组件都会在同一个地址空间内执行,它们会通过简单的函数调用来进行彼此的交互。这些系统在运行时是作为一个执行文件被统一加载到计算机内存中的。人们经常把这种模式叫作单体结构。
- 部署层次
- 服务层次:将组件间的依赖关系降低到数据结构级别,然后仅通过网络数据包来进行通信。
本章小结 141
第17章 划分边界
架构师所追求的目标最大限度地降低构建和维护一个系统所需的人力资源,那么我们就需要了解一个系统最消耗人力资源的是什么?答案是系统中存在的耦合——尤其是那些过早做出的,不成熟的决策所导致的耦合。
一个设计良好的系统架构不应该依赖于细节(数据库,web服务),而应该尽可能推迟这些细节性的决策。
几个悲伤的故事 143
FitNesse 146
应在何时、何处画这些线 148
输入和输出怎么办
I/O是无关紧要的原则。界面背后存在着一个模型——一套非常复杂的数据结构和函数,那才是游戏真正的核心驱动力。
组件图中箭头指向由不重要的组件依赖较为重要的组件。
插件式架构 152
插件式架构的好处 153
本章小结 154
第18章 边界剖析 155
跨边界调用 156
令人生畏的单体结构 156
部署层次的组件 158
线程 159
本地进程
本地进程会用socket来实现彼此的通信,当然,它们也可以通过一些操作系统提供的方式来通信,例如共享邮件或消息队列。
服务 160
本章小结
依赖箭头应该由底层具体实现细节指向高层抽象的方向。
第19章 策略与层次 162
层次(Level) 163
本章小结 166
第20章 业务逻辑 167
业务实体 168
用例
用例本质上就是关于如何操作一个自动化系统的描述,定义了用户需要提供的输入数据,用户应该得到的输出信息以及产生输出所应该采取的处理步骤。
业务实体并不会知道哪个业务用例在控制它们,这也是依赖反转原则(DIP)的另一个应用情景。业务实体这样的高层概念是无须了解像用例这样的低层概念的。反之,低层的业务用例却需要了解高层的业务实体。
请求和响应模型 171
本章小结
业务逻辑是一个操作软件系统存在的意义,它们属于核心功能,是系统用来赚钱或省钱的那部分代码。
第21章 尖叫的软件架构
一幅图书馆的建模设计图,我们应该会先到一个超大的入口,然后是一个用于签到/签出的办公区,接下来是阅读区,小型会议室,以及一排排的书架区。整个建筑设计都在尖叫着跟你说:这是一个“图书馆”。
那么,应用程序的架构设计又会“喊”些什么呢?当我们看到它的结构目录,软件包中的源码时,它们究竟是在喊“库存管理系统”,还是在喊“Spring/Hibernate”这样的技术名词呢?
架构设计的主题
我们的架构是基于框架来设计的,它就不能基于我们的用例来设计了。
架构设计的核心目标
良好的架构设计应该围绕着用例来展开。
框架应该是一个可选项,良好的架构设计允许后期再决定是否采用Spring/Hibernate这些技术。
那Web呢 175
框架是工具而不是生活信条 175
可测试的架构设计 176
本章小结 176
第22章 整洁架构
架构的特点:
独立于UI:我们可以在不修改业务逻辑的前提下将一个系统的UI由web界面替换成命令行界面。
依赖关系规则
源码中的依赖关系必须只指向同心圆的内层,即由低层机制指向高层策略。
一个常见的应用场景 183
本章小结 184
第23章 展示器和谦卑对象 185
谦卑对象模式 186
展示器与视图 186
测试与架构 187
数据库网关 188
数据映射器 188
服务监听器 189
本章小结 189
第24章 不完全边界
原则:You Aren‘t Going to Need It,不要预测未来的需要。
省掉最后一步 191
单向边界 192
门户模式 193
本章小结
我们的目标是找到设置边界的优势超过其成本的拐点,那就是实现该边界的最佳时机。
第25章 层次与边界 194
基于文本的冒险游戏:Hunt The Wumpus 195
可否采用整洁架构 196
交汇数据流 199
数据流的分割 199
本章小结 201
第26章 Main组件 203
最细节化的部分 204
本章小结 208
第27章 服务:宏观与微观 209
面向服务的架构 210
服务所带来的好处 210
运送猫咪的难题 212
对象化是救星 213
基于组件的服务 215
横跨型变更 216
本章小结 216
第28章 测试边界
测试也是一种系统组件
可测试性设计
GUI往往是多变的,因此通过GUI来验证系统的测试一定是脆弱的。
测试专用API
该API应该成为用户界面所用到的交互器与接口适配器的一个超集。
本章小结 221
第29章 整洁的嵌入式架构 222
“程序适用测试”测试
软件构建过程的三个阶段:
- “先让代码工作起来”——如果代码不能工作,就不能产生价值
- “然后再试图将它变好”——通过对代码进行重构,让我们自己和其他人更好地理解代码,并能按照需求不断地修改代码。
- “最后再试着让它运行得更快”——按照性能提升的“需求”来重构代码。
《人月神话》这本书中,Fred Brooks建议我们应该随时准备“抛弃一个设计”。
目标硬件瓶颈 228
本章小结 238
第6部分 实现细节
第30章 数据库只是实现细节 240
关系型数据库 241
为什么数据库系统如此流行 242
假设磁盘不存在会怎样
虽然硬盘现在还是很常见,但其实已经在走下坡路了。RAM正在替代一切。
我们会将数据存储为链表、哈希表等各种数据结构,然后用指针或者引用来访问这些数据——这对程序来说是最自然的方式。
实现细节 243
但性能怎么办呢 244
一段轶事
这对我来说很难接受,为什么我要将链表和树重新按照表格与行模式重组,并且用SQL方式存储呢?
本章小结 246
第31章 Web是实现细节 247
无尽的钟摆 248
总结一下 250
本章小结 251
第32章 应用程序框架是实现细节 252
框架作者 253
单向婚姻
我们与框架作者之间的关系是非常不对等的,我们要采用某个框架就意味着自己要遵守一大堆约定,但框架作者却完全不需要为我们 遵守什么约定。
风险 254
解决方案 255
不得不接受的依赖 255
本章小结 256
第33章 案例分析:视频销售网站 257
产品 258
用例分析 258
组件架构 260
依赖关系管理 261
本章小结 262
第34章 拾遗 263
按层封装 264
按功能封装 266
端口和适配器 268
按组件封装 270
具体实现细节中的陷阱 274
组织形式与封装的区别 275
其他的解耦合模式
一个稍微简单的组织方式。仅使用两个代码树:
- 业务(Domain)代码(内部)
- 基础设施(Infrastructure)代码(外部)
本章小结:本书拾遗 279
后序 280
附录A 架构设计考古 283