Skip to content

Clean Architecture

概述

cata

软件架项目特点

  • 递归(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)的一个普适版。
  • 组件聚合三大原则张力图

    • 图示

    x

    • 只关注 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:不稳定性)

    • 图示

    x

    • 主序列线:(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
    • 独立于数据库
    • 独立于任何外部机构
  • 图示

architecture

  • 依赖关系规则

    • 外层圆代表机制,内层圆代表策略。
    • 源码中的依赖关系必须指向同心圆的内层,即 由低层机制指向高层策略
    • 不应该让外层圆中发生的任何变更影响到内层圆的代码。
  • 业务实体

    • 封装整个系统的关键业务逻辑
    • 一个带方法的对象,或一组数据结构和函数的集合。
  • 用例

    • 用例引导数据在业务实体之间流入/流出,并指挥着业务实体利用其中的关键业务逻辑实现用例的设计目标。
  • 接口适配器

    • 将用例和业务实体的数据格式转化为外部系统数据格式。
    • 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 注解。