Clean Architecture¶
概述¶
软件架项目特点¶
- 递归(recursive)
- 分形((fractal)
什么是设计与架构¶
-
设计 vs 架构
- 没有区别。
- 软件架构是系统设计过程中的 重要设计决定 的集合,可以通过 变更成本 来衡量每个设计决定的重要程度。
-
架构的目标?
- 软件架构的终极目标是,用最小的人力成本来满足构建和维护系统的需求。
-
问题在哪里?
- 持续低估那些好的、良好设计的、整洁的代码的重要性。
- 产品经理的鬼话:“我们可以未来再重构代码,产品上线最重要!”
-
设计原则的意义
- 于私:原则是自我改善的工具,日常验证自己的方法论,快速成长。
- 于公:超脱环境和情绪的影响,自觉选择最佳方案。
两个价值维度¶
-
行为价值
- 程序员的工作是且仅是:按照需求文档编写代码,并修复任何 Bug。
-
架构价值
- Software:“ware” 指产品,“soft” 指软件的灵活性。软件必须保持灵活性,即易于修改。
- 变更实施的难度应该和变更的 范畴(scope) 等比相关,而与变更的具体 形状(shape) 无关。
- 好的系统架构设计应该尽可能做到与 “形状” 无关。
-
哪个价值维度更重要?
- 架构价值维度。只有易于修改,才能让程序持续产生价值。
-
艾森豪威尔矩阵
- 重要且紧急
- 重要不紧急
- 不重要但紧急
- 不重要且不紧急
编程范式¶
编程范式总览¶
- 编程范式即程序的编写模式,与编程语言关系相对较小。
- 范式转移 (paradigm shift)
结构化编程¶
-
goto 是有害的
- Dijkstra 发现:goto 语句的某些用法会导致某个模块无法被递归拆分成更小的、可证明的单元。
-
可推导性
- Bohm 和 Jocopini 证明:人们可以用顺序结构、分支结构、循环结构这三种结构构造出任何程序。
- 证明了我们构建可推导模块所需要的控制结构集,与构建所有程序所需要的控制结构集的最小集是等同的。 如此,结构化编程诞生。
-
功能性降解拆分
- 结构化编程范式可将模块递归降解拆分为可推导单元;意味着大型系统设计可拆分成模块和组件,进而拆分为更小的、可证明的函数。
-
科学证明与测试
- 科学理论/定律的特点:它们可以被证伪,但无法被证明。
- Dijkstra:测试只能展示 Bug 的存在,但不能证明不存在 bug。
- 一段程序永远无法被证明是正确的,但不妨碍它有效。
面向对象编程¶
-
封装
- C++ 作为一种面向对象的编程语言,反而破坏了 C 的完美封装性。
-
继承
-
多态
- 面向对象的编程语言让多态变得安全且便利。
- 面向对象的编程其实是对程序间接控制权的转移进行了约束。
-
依赖反转 (DI)
- 源代码上的依赖关系(依赖 Interface)的方向,与控制流正好相反。
- 面向对象编程范式的核心本质:软件架构不再受系统控制流的限制。
- 业务逻辑组件不会依赖于用户界面和数据库这两个组件。
-
总结
- 面向对象编程就是以 多态 的手段对源代码中的 依赖关系进行控制 的能力。
- 这种能力让软件架构师可以构建出某种 插件式架构 ;
- 让 高层策略性组件 与 底层实现性组件 相分离;
- 底层组件可以被编译成插件,实现独立于高层组件的开发和部署。
函数式编程¶
-
不可变性与软件架构
-
为什么关注变量的可变性?
- 所有竞争问题、死锁问题、并发更新问题丢失由可变变量导致的。
-
函数式编程语言中的变量是不可变的。
-
-
可变性的隔离
- 通常采用某种 事务型内存 来保存可变变量,避免同步时更新和竞争状态的发生。
- 要点:好的架构应将可变状态和不可变状态隔离成单独组件,然后用合适的机制来保护可变量。
- 架构师应将 大部分逻辑归于不可变组件,可变组件逻辑越少越好。
-
事件溯源
- 只存储 事务记录,不存储 具体状态;当需要具体状态时,只需从头计算所有事务记录即可。
- 假定存储量和计算能力足够大,应用程序可用 完全不可变的、纯函数式 的方式来编程。
设计原则¶
SRP:单一指责原则¶
-
概念
- 误解:每个模块都应该只做一件事。
- 正解:任何一个模块应该有且仅有一个被修改的原因。 (即 actor:用户或 Stakerholder)
- 最终定义:任何一个软件模块都应该只对一类行为者 (actor) 负责。
-
起源
- 源于康威定律的推论:一个软件系统的最佳结构,高度依赖于开发系统的组织的内部结构。
- 因此,每个软件模块有且只有一个需求改变的理由。
-
本质
- 讨论函数与类之间关系。
OCP:开闭原则¶
-
概念
- 良好的设计应该易于扩展,抗拒修改。
-
依赖方向的控制
- 若 A 组件(高阶组件)不想被 B 组件(低阶组件)的修改影响,则应让 B 依赖于 A。
-
案例
- 图中 “使用” 关系(开放箭头),它和控制流的方向一致;而 “继承” 关系(闭合箭头)则与之相反。
- 体现对开闭原则的应用,通过调整依赖关系,保证底层细节变更不会影响到高层策略组件。
LSP:里氏替换原则¶
ISP:接口隔离原则¶
- 任何层次的组件设计,如果依赖于不需要的东西,都是有害的。
DIP:依赖反转原则¶
-
概念
- 源码的依赖关系中多引用抽象,而非具体实现。
- 源代码依赖方向,永远是控制流方向的反向。
-
编码守则
- 应在代码中多使用抽象接口,尽量避免使用多变的具体实现。
- 不要在实现类上创建衍生类。
- 不要覆盖 (override) 包含具体实现的函数。
- 避免在代码中写入与任何具体实现相关的名字,或是其他容易变动的事物的名字。
-
抽象层的稳定性
- 抽象比实现更稳定。
-
工厂模式
- 解决源代码依赖问题:具体实现层依赖抽象层。
-
架构边界
- 划分 抽象接口 与其 具体实现。
- 所有跨越此边界的依赖关系都是单向的。
组件构建原则¶
组件¶
- 组件是软件的部署单元,是整个软件系统在不是过程中可以独立完成部署的最小实体。
-
墨菲定律
- 程序的规模会一直不断地增长下去,直到将有限的编译和链接时间填满为止。
-
摩尔定律
组件聚合¶
-
REP:复用/发布等同原则
- 软件复用的最小粒度等同于其发布的最小粒度。
- 为 复用性 为组合。
-
CCP:共同闭包原则
- 同时、且 由于相同原因 而修改的 类,放在一个组件中。
- 为 维护性 而组合
- 单一指责原则(SRP)的再度阐述。
-
CRP:共同复用原则
- 不要强迫一个组件的用户依赖他们不需要的东西。
- 为 避免不必要发布 而切分。
- 接口隔离原则(ISP)的一个普适版。
-
组件聚合三大原则张力图
- 图示
- 只关注 REP + CCP,引发很多不必要的发布。
- 只关注 REP + CRP,导致简单变更影响多个组件;
组件耦合¶
-
无依赖环原则 (ADP)
-
定义
- 组件依赖关系图中不应该出现环。
-
每周构建
- 消除循环依赖(DAG,Directed Acycle Graph 有向无环图)
-
如何打破循环依赖
- 1)应用依赖反转原则;
- 2)创建新的组件,让造成循环依赖的两个组件都依赖于新组件。
-
抖动
- 当循环依赖出现时,随着无循环原则 (ADP) 的应用,组件依赖关系会产生相依的抖动和扩张。
-
-
稳定依赖原则 (SDP)
-
定义
- 依赖关系必须要指向更稳定的方向。(有点像贪心算法)
-
什么是稳定性?
- 组件难于修改,即让很多其他组件依赖与它。
-
稳定性的指标
-
位置稳定性 (positional stability)
- Fan-in: 入向依赖
- Fan-out: 出向依赖
- I: 不稳定性,
I = Fan-out / (Fan-in + Fan-out)
;0 最稳定,1 最不稳定。
-
SDP 要求 每个组件的 I 大于其所依赖的组件的 I 。(贪心思想)
-
-
并非所有组件都应稳定
- 可变组件(Flexible) 位于顶层,依赖于底层的稳定组件 (Stable)。
- 若出现
Stable -(依赖)-> Flexible
,则违反 SDP;需要应用 DIP 来修复; - 创造一个接口类,强迫 Stable 和 Flexible 都依赖于它,打破原有依赖关系。
-
-
稳定抽象原则 (SAP)
-
定义
- 一个组件的抽象化程度应该于其稳定程度保持一致。
-
Why?
- 高阶策略应放在哪里?稳定组件 (I=0) 中。
- 抽象类:良好支持开闭原则 (OCP)。
-
What?
- 一个稳定组件,最好由 接口和抽象类 组成,以便扩展。
- SAP + SDP = 组件层次上的 DIP;即, 依赖关系应该指向更抽象的地方 。
-
抽象化指标
- Nc:组件中类的数量
- Na:组件中抽象类和接口的数量
- A:抽象程度,
A = Na / Nc
。
-
-
抽象稳定坐标系(纵轴为 A:抽象性,横轴为 I:不稳定性)
- 图示
- 主序列线:(0, 1) 到 (1, 0) 的连线,线两侧为合理区间。
-
痛苦区:(0, 0) 附近
- 稳定且具体的区域,难以被修改。
- 如可变性上臭名昭著的表结构 (schema):即具体,又被众多组件依赖;一旦更改,非常痛苦。
- 若不可变组件落在此区域,则无害。
-
无用区:(1, 1) 附近
- 无限抽象,但又没被其他组件依赖。
- Base classes in a language like String, Bool and so on.
-
避开以上两区域
- 软件最优位置是主序列线两端;
- 架构经验中,不可能做到完全抽象,也不可能做到完全稳定,需尽可能贴近主序列线即可。
-
离主序列线距离 (D)
D = |A + I - 1|
- D 指标,来指导组件的重构和重新设计。
- D 指标的平均值和方差,来量化分析一个系统设计。方差可于用作组件的 “达标红线”。
软件架构¶
什么是软件架构¶
-
架构师
- 软件架构师是能力最强的程序猿,需要自身承接编程任务的同时,逐渐引导整个团队向一个能够最大化生产力的系统设计方向推进。
-
终极目标
- 最大化程序员生产力,最小化运营成本。
-
开发
- 避免开发进度驱动架构设计
-
部署
- 目标:一键式部署
-
运行
- 一个良好的软件架构应该能明确的反映该系统在运行时的需求。
-
维护
- 软件系统所有方面中,维护所需成本最高。
- 探秘 & 风险
-
保持可选项
- 软件架构之“软”:尽可能长时间保持更多的可选项。
-
设备无关
- 高层策略与底层实现隔离
-
系统元素
- 策略元素: 业务规则与操作过程
- 细节元素:功能实现细节
- 目标:以策略为最基本元素,细节与策略脱离关系。
独立性¶
- 一个设计良好的架构应该能允许一个系统, 从单体结构开始,以单一文件的形式部署;
- 然后成长为一组相互独立的可部署单元,甚至是 独立的服务或微服务;
- 最后还能随着情况变化,允许系统逐渐回退到单体结构。
-
解耦模式
- 源码层次
- 部署层次
- 服务层次
划分边界¶
-
What
- 软件架构是一门 划分边界的艺术。
-
过早且不成熟的决策
- 框架,数据库,Web 服务器,工具库,依赖注入等。
- 一个良好的系统架构中,细节性的决策都是辅助性的,应该尽可能地推迟。
-
通过划清边界,推迟和延后一些细节性的决策,最终节省大量时间,避免大量问题。
-
When: 应在何时,何处画线
- 业务逻辑组件 & 数据库组件
Business Rules -(关联)-> Database Interface <-(泛化)- Data Access -(关联)-> Database
- 画一条线:
Business Rules <-(关联)- Database
- 依赖倒置原则
-
How:确定输入 & 输出
- 如何定义系统边界
-
插件式架构
- 架构史就是一个如何通过插件方式,构建可扩展、可维护系统架构的故事。
- 插件式架构,等于构建一道防火墙。墙两侧组件应该以不同原因、不同速率变更。
边界剖析¶
- 跨边界调用
-
邪恶的单体结构
- 源码层次的解耦 模式:同一个进程、同一个地址空间的函数和数据划分。
- 单体结构中,组件间的交互一般通过普通函数,迅速而廉价 ;意味着跨源码层次解耦边界的通信会很频繁。
- 魔鬼(邪恶,爱作弄人)从来不睡觉,因为藏在人思维里。
-
部署层次的组件
- 物理边界形式:动态链接库。
-
服务
- 系统架构中最强的边界形式。
- 通信速度慢,通信次数少
-
小结
- 一个系统中通常会同时包含:高通信量、低延迟的 本地架构边界 ,和低通信量、高延迟的 服务架构边界 。
策略与层次¶
-
策略
- 软件系统本质上是一组 策略语句的集合。
-
策略语句分类
- 描述计算部分的业务逻辑
- 描述计算报告的格式
-
变更原因、时间和层次相同的策略分到同一组件;反之,不同组件。
-
层次
-
定义
- 一组策略 距离系统的输入/输出越远,它所属的层次 越高。
- 直接管理输入/输出的策略层次最低。
-
依赖反转DIP:希望源码中的依赖关系与其 数据流向脱钩,与组件所在 层次挂钩。
- 低层组件被设计为依赖高层组件。
- 低层组件应成为高层组件的插件,
-
业务逻辑¶
-
业务实体
- 定义:描述业务逻辑的结构。
- 由 关键业务逻辑 和 关键业务数据 组成。
-
用例
- 定义:描述特定应用场景下的业务逻辑。
- 依赖反转DIP:业务实体并不知道哪个业务用例来控制它;相反,业务用例需要了解业务实体。
- 业务实体远离输入/输出,层次高;用例更靠近输入/输出,层次低。
-
请求/响应
- 业务实体与请求/响应模型之间很多数据相同,但请不要在业务实体中直接引用!
- 因为随着时间推移,会以不同原因、不同速率变更。
- 若聚合,则违反 共同闭包原则 (CCP) 和 单一职责原则 (SRP)。
尖叫的软件架构¶
-
什么是尖叫?
- 架构的设计自我解释,即解释架构设计的主题。
-
架构设计的主题
- 系统架构应为该系统的用例提供支持,即业务用例驱动的设计方式。
- 系统架构图应彰显其主题(是住宅还是图书馆),且凸显该有哪些业务用例。
- 架构不该基于框架设计,而应基于用例来设计。
-
架构设计的核心目标
- 应该只关注用例。
- 尽可能允许用户推延决定采用什么框架、数据库、Web 服务器等。
- 框架应该是一个可选项。
-
框架是工具而不是生活信条
- 不要让框架成为你的观点。
- 避免让框架主导我们的架构设计。
整洁的架构¶
-
整洁架构举例
- 六边形架构:《Growing Object Oriented Software with Tests》
- DCI 架构
- BCE 架构:《Object Oriented Software:A Use-Case Driven Approach》
-
特点
- 独立于框架
- 可被测试
- 独立于 UI
- 独立于数据库
- 独立于任何外部机构
-
图示
-
依赖关系规则
- 外层圆代表机制,内层圆代表策略。
- 源码中的依赖关系必须指向同心圆的内层,即 由低层机制指向高层策略 。
- 不应该让外层圆中发生的任何变更影响到内层圆的代码。
-
业务实体
- 封装整个系统的关键业务逻辑
- 一个带方法的对象,或一组数据结构和函数的集合。
-
用例
- 用例引导数据在业务实体之间流入/流出,并指挥着业务实体利用其中的关键业务逻辑实现用例的设计目标。
-
接口适配器
- 将用例和业务实体的数据格式转化为外部系统数据格式。
- GUI MVC 框架属于此层:Model 由 Controller 传给用例,再由用例传回展示器和 View。
- SQL 语句都应该被限制在此层。
-
跨越边界
- 原则:内层圆中的代码不能引用其外层的声明。
- 利用多态技术,将源码中的依赖关系与控制流方向进行反转。
- 利用 DIP
-
框架与驱动程序
展示器和谦卑对象¶
-
谦卑对象模式
- 将单元测试行为根据难易分组隔离;谦卑组包含难以测试的行为,简化到不能再简化。
-
测试与架构
- 将系统行为分割成可测试和不可测试部分的过程,定义了系统的架构边界。
-
系统边界处使用谦卑对象模式,可以大幅度提高系统的可测试性。
- GUI 拆为展示器和视图,视图部分属于谦卑对象。
架构边界¶
-
不完全边界
- 构建完整架构边界成本太高,需要先引入不完全边界 (partial boundary)。
-
层次与边界
- 架构边界可以存在于任何地方。
- 架构师需要权衡成本,是否需要设置边界,何处设置边界,是完整边界,还是不完全边界,
Main 组件¶
- 架构中最细节化的部分。
- 可被视为插件,负责设置起始状态,配置信息,加载外部资源,最后将控制权转交给高层组件。
服务:宏观与微观¶
-
服务不是架构
- 服务只是一种比函数调用成本稍高的,分割应用的一种形式,与架构无关。
- 系统架构都是由那些跨越架构边界的关键函数调用来定义的,且必须遵守依赖关系规则。
-
面向服务架构的好处?
- 解耦合的谬论
- 独立开发部署的谬论
-
运送猫咪的难题
- 所谓的 横跨型变更 问题:全部服务都需要变更。
-
对象化是救星
- 模板方法模式 或 策略模式。
-
基于组件的服务
测试边界¶
-
测试也是一种系统组件
- 遵循依赖关系原则:最外圈程序,向内依赖。
-
可测试设计
- **脆弱的测试问题 ** :修改一个通用系统组件可能会导致成百上千个测试问题出现。
- 软件设计第一条原则:不要依赖于多变的东西。
-
测试专用 API
- 结构性耦合
- 安全性:避免超级权限 API 被部署到生产。
简洁的嵌入式架构¶
-
固件的定义和误解
-
定义
- 软件本身不会随着时间推移而磨损,但硬件及其固件会碎时间推移而过时。
- 未妥善管理的硬件依赖和固件依赖是软件的头号杀手。
-
BAD CASE
- 工程师被问:如何处理某个逻辑?工程师消失一段时间,然后给出非常具体的答案。被问:从哪查到这个结果?回答:“从当前的产品代码里!”
- 整个产品已经成为事实上的固件。
-
多写软件,少写固件,让代码活得更久一点。
-
-
“程序适用测试” 测试
- 解释了:为什么这么多嵌入式软件最后都成了固件?
-
软件构建三阶段
- 1)“先让代码工作起来”
- 2)“然后再试图将它变好”
- 3)“最后再试着让它运行的更快”
-
程序适用测试 ( app-titude test):停留在第一阶段,目标仅仅是让程序工作。
实现细节¶
数据库只是实现细节¶
- 数据库不是数据模型,而是存取数据的工具,底层的实现细节。
- 一个优秀的架构师是不会让实现细节污染整个系统架构的。
-
但性能怎么办?
- 可以在数据访问层解决,而不能让它与系统架构相关联。
Web 是实现细节¶
应用程序框架是实现细节¶
-
单向婚姻
- 与框架作者关系不对等:自己要遵守一堆约定,而框架作者不需要。
-
风险
-
框架本身可能经常违反依赖关系原则。
- 比如,element-ui 的文件上传插件,违反 “web 控制器永远不应该直接访问数据层。”
- 譬如,框架可能要求我们将代码引入业务实体,造成耦合发生在最内圈。
- “宽松的分层架构”
-
随着时间推移,功能要求会大于框架所能提供的范围。与框架斗争时间大于框架帮助我们的时间。
- 框架可能朝着我们不需要的方向演进。
- 未来可能出现更好的框架。
-
-
解决方案
- 请不要嫁给框架!
- 时刻警惕,不要让框架进入内圈。
- 譬如,依赖注入框架 Spring,不要在业务对象中到处写
@Autowired
注解。