Clean Architecture

June 18, 2021

第一部分 概述

第 1 章 设计与架构

  • 随着软件版本的更迭: 需要的工程师人数越来越多、人均生产效率显著降低(以同期代码行数作为统计)、每行代码的变更成本显著增高.
  • 对于重构的过于乐观: 为了快速上线而容忍混乱的代码, 忽略软件架构, 而寄希望于未来的重构工作. 事实上, 新功能源源不断, 混乱的架构又会导致新功能的开发成本急速上升, 生产效率持续下降, 陷入恶性循环, 导致重构的成本越来越大, 重构的时机几乎不会存在.

第 2 章 两个价值维度

  • 两个价值纬度

      1. 系统行为, 即软件功能是否满足需求
      1. 系统架构, 即软件是否足够灵活
  • 何者更重要?

    • 是系统正常工作更重要, 还是系统易于修改更重要?
    • 对于业务部门来说, 答案一般是前者, 而一旦开发人员也选择了前者, 紧接着就会面临源源不断的新增需求和需求变更, 导致生产效率直线下降.
    • 所以对于开发人员来说, 系统架构大于系统行为: 只要保持系统的灵活性, 系统行为总会以平和的方式得到满足.
    • 开发团队同市场、销售、运营团队一样需要“长期抗争”, 保护系统的灵活性/可维护性, 是开发团队的职责.

第二部分 编程范式

第 3 章 编程范式总览

  • 三大编程范式

    • 结构化编程
    • 对程序控制权的直接转移进行了限制和规范
    • 限制了 goto: 使用 if、for、while 等流程控制
    • 面向对象编程
    • 对程序控制权的间接转移进行了限制和规范
    • 限制了函数指针
    • 函数式编程
    • 对程序中的赋值进行了限制和规范
    • 限制了赋值语句
  • 编程范式与架构

    • 架构的三大关注点
    • 功能性
    • 组件独立性
    • 数据管理
    • 编程范式的作用
    • 结构化编程: 实现逻辑功能
    • 面向对象: 封装与多态
    • 函数式: 规范数据存放与访问权限

第 4 章 结构化编程

  • 可推导性

    • 三种基本结构
    • 顺序结构
    • 分支结构
    • 循环结构
    • 可以用三种基本结构构造出任何程序
  • 不受限制的直接控制转移语句——goto 是有害的, 可被三种基本结构替代
  • 结构化编程范式使得可以将大型系统设计拆分为模块和组件, 然后可递归拆分为更小的、可证明的函数
  • 结构化编程范式促使将一段程序递归降解为一系列小单元, 程序的测试过程即证伪过程.

第 5 章 面向对象编程

  • 面向对象编程语言

    • 封装性: 较 C 而言实际上减弱了封装性
    • 继承性: 提供了一定便利,但没有开创出新
    • 多态: C 本来就有,只是提供了安全性和便利
  • 面向对象的多态

    • 插件式架构
    • 程序应与设备无关
    • 依赖反转
    • 原本: 层层依赖-上层组件依赖下层组件
    • 多态: 上层组件提供需求接口, 底层组件实现接口
    • 例子
    • 原本: 业务逻辑引入(依赖)用户界面和数据库
    • 应用多态: 用户界面和数据库作为业务逻辑的插件,从而可以各自独立部属
  • 架构师眼中的面向对象

    • 以多态为手段来对源代码中的依赖关系进行控制, 构建出某种插件式架构, 让高层策略性组件和底层实现性组件相分离, 底层组件可以作为插件, 独立于高层组件进行开发和部属

第 6 章 函数式编程

  • 变量不可变
  • 不可变性与软件架构

    • 如果变量不可变, 一切并发问题都会不复存在: 竞争、死锁、并发更新等.
    • 不可变性是否可行? 如果能忽略存储器和处理器的速度限制,则可行; 否则只有一定情况下可行
  • 可变形隔离

    • 一个架构设计良好的应用程序应该将状态修改的部分和不需要修改状态的部分隔离成单独的组件,然后用合适的机制来保护可变量
    • 软件架构师应该着力于将大部分处理逻辑都归于不可变组件中,可变状态组件的逻辑应该越少越好
  • 事件溯源

    • 如果有足够大的存储量和处理能力,应用程序就可以用完全不可变的、纯函数式的方式来编程
    • 只存储事务记录, 不存储具体状态, 通过计算所有事务来获取当前状态

第三部分 设计原则

综述

  • 构建中层结构

    • 使软件可容忍被改动
    • 使软件更容易被理解
    • 构建可复用的组件
  • SOLID 原则

    • SRP: 单一职责原则——每个模块有且只有一个被改变的理由
    • OCP: 开闭原则——允许通过新增代码来扩展功能,尽量减少代码修改
    • LSP: 李氏替换原则——遵守同一约定的组件可相互替换
    • ISP: 接口隔离原则——在设计中避免不必要的依赖
    • DIP: 依赖反转原则——实现底层细节的代码应依赖高层策略性代码,而非反向依赖

第 7 章 SRP: 单一职责原则

  • 不仅仅是“每个模块只做一件事”
  • 任何一个软件模块都应该只对某一类行为者负责

第 8 章 OCP: 开闭原则

  • 易于扩展(新增代码),抗拒修改
  • 实现方法

      1. 将需求分组,即 SRP
      1. 调整分组之间的依赖,即 DIP
  • 如果 A(Father)组件不想被 B(Child)组件发生的修改所影响,那么就让 B 依赖于 A.
  • 分层设计: 高层组件更核心、更封闭, 低层依赖于高层.

第 9 章 LSP: 李氏替换原则

  • 子类型可以替代父类型被调用/使用, 即继承关系
  • LSP 演变为更广泛的、指导接口与实现方式的设计原则
  • 反例: 当核心业务逻辑出现无法避免的例外情况时,避免在核心组件中进行特殊情况判断,而应该使用一个额外的调度组件来处理特殊情况

第 10 章 ISP: 接口隔离原则

  • 尽量避免多个行为依赖并操作同一个接口: 在中间再做一层封装进行隔离.
  • 尽量减少不必要的依赖, 第 13 章再继续探讨更多细节

第 11 章 DIP: 依赖反转原则

  • 在源代码层次上只引用包含接口、抽象类或其他抽象类型声明的源文件, 而不引用任何具体实现.
  • 编码守则

    • 多使用抽象接口,避免使用多变的具体实现类
    • 不要在具体实现类上创建衍生类
    • 不要覆盖(override)包含具体实现的函数
    • 避免在代码中写入任何具体实现相关或其他容易变动的事务的名字
  • 使用抽象工厂模式创建对象

第四部分 组件构建原则

第 12 章 组件

  • 组件是软件在部属过程中的最小单元

    • 在编译运行语言中是一组二进制文件;

    在解释运行语言中是一组源代码文件

  • 设计良好的组件: 独立部署、单独开发
  • 组件概念的历史: 动态链接文件
  • 现状: 组件化的插件式架构

第 13 章 组件聚合

  • 问题: 哪些类应该被合成一个组件?
  • 基本原则

    • REP: 复用/发布等同原则

      - 软件复用的最小粒度应等同于其发布的最小粒度
      - 被复用的组件应有明确的发布版本号、适当的通知和发布文档
      - 组件中包含的类与模块也应该可以同时发布,共享相同的版本号和版本跟踪,被包含在发布文档中.
      
    • CCP: 共同闭包原则

      - 将会同时修改、并且为相同目的而修改的类放入同一组件; 反之放入不同组件.
      - SRP原则在组件层面的阐述
      - 一个组件应该只有一个变更原因;
      

    一次变更最好都体现在一个组件中 - 一般来说, 可维护性比可复用性重要得多

    • CRP: 共同复用原则

      - 将经常共同复用的类和模块放入同一个组件
      - 不是紧密相连的类不应被放入同一组件
      - 是ISP原则的普适版
      
  • 组件聚合原则张力图
  • REP 和 CCP 使组件更大, CRP 使组件更小, 架构师的任务就是在三原则中进行取舍, 并且是随着项目状态逐步调整

    • 项目早期一般偏向右侧, 主要牺牲复用性
    • 随着项目逐渐成熟,其他项目对其产生依赖,会逐渐向左侧滑动

第 14 章 组件耦合

  • 无依赖环原则

    • 解决方案
      1. 将项目划分为可独立发布的组件
      1. 组件独立发布,打版本号并通知其他成员
      1. 其他开发者基于组件公开发布的版本进行开发,并可以选择是否采用新版本
    • 组件依赖图: 有向无环图(DAG)
    • 可以直观地判断出某个组件变更的影响范围
    • 无环: 从任意节点开始沿依赖线都回不到起始点
    • 发布过程从下至上进行编译、测试、发布
    • 循环依赖: 组件依赖图存在环, 组件的独立维护工作以及单元测试、发布流程都将十分困难
    • 消除循环依赖
      1. 应用依赖反转原则(DIP), 将环形依赖反转
      1. 创建新的上层组件, 将相互依赖的类进行抽象提取
    • 当循环依赖出现时,必须立刻进行消除,调整组件结构
    • 组件结构图的构建
    • 不可能在系统构建之初就被完美设计, 因为它不是描述软件功能的,而是软件构建性和维护性的地图
    • 隔离频繁的变更: 将稳定的高价值组件与常变的组件进行隔离
    • 随着项目的逻辑设计一起扩张和演进
  • 稳定依赖原则(SDP)

    • 稳定性
    • 直观上

      • 如果组件不依赖其他组件, 或被多个其他组件依赖, 则是稳定的组件.
      • 如果组件依赖多个组件, 则是不稳定的
    • 指标

      • I(不稳定性) = FanOut / (FanIn + FanOut)
      • FanIn: 入向依赖,组件内部类被外部类依赖的数量
      • FanOut: 出向依赖, 组件内部类依赖外部类的数量
    • 要求: 每个组件的 I 指标都大于其所依赖组件的 I 指标. 即: 越高层的组件越稳定.
    • 高阶组件 >> I=0
  • 稳定抽象原则(SAP)

    • 组件的抽象化程度应与其稳定性保持一致,
    • 如何使一个趋于无限稳定(I=0)的组件接收变更?
    • 抽象类(与接口)
    • 指标
    • A(抽象化程度) = Na / Nc

      • Na: 组件中类的数量
      • Nc: 组件中抽象类和接口的数量
      • 0 表示没有抽象类; 1 表示只有抽象类
  • SDP 与 SAP

    • I/A 图
    • 最稳定、包含无限抽象类的组件位于左上角(0,1)
    • 最不稳定、最具体的组件位于右下角(1,0)
    • 主序列、痛苦区与无用区
    • 痛苦区: 稳定且具体, 涉及很多具体业务但又难以修改

      • 典型: 数据库的表结构、工具型类库
    • 无用区: 无限抽象、但没有被其他组件依赖, 多为无用代码
    • 主序列线: 合适的位置

      • 尽量让组件贴近主序列线
      • 最优位置是线的两端
    • D 指标 = |A + I - 1|
    • 组件与主序列线的距离: 0 表示在主序列上; 1 表示最远位置
    • 可用“D 指标小于 xx”来指导组件的重构
    • 用途

      • 重点分析 D 指标处于平均值的标准差之外的组件: 要么过于抽象但依赖不足, 要么过于具体而被依赖太多
      • 按时间跟踪每个组件的 D 指标, 及时发现组件架构隐患

第五部分 软件架构

第 15 章 什么是软件架构

  • 软件架构师: 坚持一线程序员、更多的编程任务
  • 实质: 如何将系统切分成组件, 并安排好组件之间的排列关系及互相通信的方式.
  • 目的: 更好地对组件进行研发、部属、运行及维护

    • 开发: 当开发人员组成复杂、系统体量较大时, 清晰的组件和稳定的接口是开发顺利的必要条件
    • 部属: 一键式的轻松部属应该是设计软件架构的目标.
    • 微服务架构虽然有利于开发, 但要考虑其部属和通信带来的隐患
    • 运行: 软件架构对运行的影响较小, 但架构应该将系统中的用例、功能和核心行为设为开发者可见的一级实体, 简化理解
    • 维护: 成本最高的部分
    • 主要成本

      • 探秘: 对现系统的挖掘, 确定新增功能或被修复问题的最佳位置和方式
      • 风险: 进行修改时, 对可能衍生出新问题的风险成本
    • 通过架构设计(切分、隔离组件)降低以上成本
  • 策略: 保持可选项

    • 软件系统的主要元素
    • 策略: 业务规则与操作过程, 是系统的价值所在
    • 细节: 用户、程序员或第三方与策略进行交互的行为, 包括 I/O 设备、数据库、Web 系统、服务器、框架、交互协议等
    • 架构: 以策略为基本元素, 让细节与策略脱离关系, 并允许在具体决策过程中推迟或延迟与细节相关的内容
    • 方法: 做高层的策略决策时,尽可能摆脱并推迟对细节的决策(如数据库、框架和设备的选型等)
    • 好处

      • 对策略的信息越多, 对细节的决策越合理
      • 保持可选项, 可尝试不同的细节决策
    • 示范: 设备无关性的应用

第 16 章 独立性

  • 架构的支持目标

    • 用例: 架构的首要目标是为所有系统用例提供支持
    • 运行: 架构需要支持系统的运行条件
    • 如为了支持系统的吞吐量和响应时间要求, 使用微服务或多进程、多线程架构
    • 开发: 将系统切分为隔离良好、可独立开发的组件
    • 部属: 设计目标是“一键部属”, 减少部属脚本与配置文件
  • 挑战: 无法预知所有用例、运行条件、开发团队结构和部属需求; 并且这些需求会发生变化
  • 策略: 保留可选项

    • 用例: 解耦模式
    • 水平分层解耦

      • UI 界面
      • 应用独有的业务逻辑
      • 领域通用的业务逻辑
      • 数据库
      • ...
    • 垂直解耦: 按用例(如新增、删除)对系统进行垂直切分, 每个用例都可能涉及 UI 界面、业务逻辑和数据库
    • 运行: 按用例解耦后, 可以将高吞吐量和低吞吐量的组件、UI 和数据库等按需分开部属在不同的环境中
    • 开发: 解耦后可按水平分层或用例分别独立开发
    • 部属: 解耦后可独立部署、热更新等
  • 重复的代码不一定是坏事

    • 用例之间的重复代码: 可能之后的变更速率和变更缘由会完全不同, 必须加倍小心地避免在用例之间复用代码
    • 水平分层的重复代码: 当数据库结构与 UI 界面的数据接口非常相似时(几乎一定是表面性的重复),也不要省略中间的视图模型,要保持水平分层之间的隔离
  • 解耦

    • 模式
    • 水平分层
    • 用例解耦
    • 解耦层次
    • 源码层次: 源代码模块之间通过函数调用来交互
    • 部属层次: 部属单元(jar 包、DLL、共享库)之间通过函数调用、跨进程通信、socket 或共享内存通信
    • 服务层次: 组件之间仅通过网络数据包通信
    • 解决方案
    • 单一层次

      • 源码层次: 适合系统只运行在一台服务器上(单体结构), 但之后可能需要进行部属层次和服务层次的解耦
      • 服务层次: 资源成本、研发成本、人力成本高昂
    • 动态层次

      • 源码层次 -> 部属层次 -> 服务层次
      • 根据系统开发和部属需要进行变更
    • 良好的架构
    • 允许从单体结构向可部属单元、独立的服务或微服务进行转变
    • 允许从部属和服务层次回退到单体结构
    • 在层次转变过程中保持系统的大部分源码不受影响

第 17 章 划分边界

  • 划分边界的目的: 尽量将一些决策延后, 并确保这些决策不对核心业务逻辑产生干扰
  • 系统最消耗人力资源的问题: 耦合

    • 尤其是与系统业务需求无关的决策造成的耦合, 如过早决策系统框架、数据库、服务器等
  • 范例分析

      1. 过早地做出决策去适应一个并不存在的大型服务器集群环境,导致开发成本急剧上升.
      1. 过早地采用一整套域对象服务体系,需要将一整套服务全部运行起来才能进行开发,导致开发效率急剧下降
      1. 成功案例: 延后数据库相关决策,采用一种与数据库无关的设计,并预留空的数据访问方法,使得开发过程中不需要面对表结构问题、查询问题、数据库服务器问题、密码问题、链接时间等一系列数据库带来的问题
  • 在何处划分?

    • GUI 与业务逻辑之间
    • 数据库与 GUI 之间
    • 数据库与业务逻辑之间
  • 插件式架构

    • 核心业务逻辑与其他组件隔离
    • 其他组件要么是可去掉的, 要么是有多种实现的
    • GUI 与数据库都应可作为插件进行替换
    • 是单一职责原则(SRP)的具体实现

第 18 章 边界剖析

  • 跨边界调用: 边界一侧的函数调用另一侧的函数,并同时传递数据
  • 单体结构/源码层次

    • 单体结构: 各组件合并产生一个单独的可执行文件
    • 一般利用某种动态形式的多态来管理内部依赖关系
    • 最简单的调用形式: 低层客户端调用高层服务函数
    • 当高层组件需要调用低层组件中的服务时,可以运行动态形式的多态来反转依赖关系
    • 高层组件提供接口
    • 低层组件实现接口并被高层组件调用
  • 部属层次

    • 跨边界调用方式与单体结构类似, 只是普通的函数调用.
  • 线程模型: 单体结构和按部属层次划分的组件都可以采用线程模型
  • 本地进程

    • 不同进程拥有不同的地址空间, 无法共享内存
    • 进程间通信
    • 用某种独立的内存区域实现共享
    • socket(最常见)
    • 一些操作系统提供的方式,如共享邮件、消息队列
    • 进程间的隔离策略与单体结构类似, 依赖关系始终指向更高层次组件
    • 高层进程源码中不应包含低层进程的名称、物理地址或注册表键名.
    • 设计目标: 低层进程作为高层进程的插件
    • 进程间通信成本相对较高,需要谨慎控制通信次数
  • 服务

    • 系统架构中最强的边界形式
    • 不依赖于具体的运行位置,始终假设服务之间的通信全部通过网络进行
    • 跨边界通信速度缓慢,尽可能控制通信次数并适应高延时情况
    • 目标: 低层服务成为高层服务的插件

第 19 章 策略与层次

  • 策略

    • 程序 = 策略语句的集合
    • 策略语句
    • 描述计算部分的业务逻辑
    • 描述计算报告的格式
    • 描述输入数据的校验策略
    • 架构设计
    • 将策略语句彼此分离, 按变更方式(原因、时间、层次)重新分组(组件)
    • 将组件重新组合为一个有向无环图, 低层依赖于高层
  • 层次

    • 按输入与输出之间的距离来分层, 距离越远层次越高
    • 高层提供接口, 低层实现接口并依赖高层
  • 涉及原则

    • 单一职责原则(SRP)、开闭原则(OCP)、共同闭包原则(CCP)、依赖反转原则(DIP)、稳定依赖原则(SDP)以及稳定抽象原则(SAP)

第 20 章 业务逻辑

  • 应用程序

    • 业务逻辑
    • 插件
  • 业务实体(Entity)

    • 构成
    • 关键业务逻辑
    • 关键业务数据: 包含或容易访问
    • 接口
    • 实现关键业务逻辑的函数
    • 操作关键业务数据的属性或函数
  • 用例(Usecase)

    • 定义输入、输出及产生输出的过程
    • 描述某种特定应用场景下的业务逻辑
    • 用例属于低层概念, 依赖于业务实体
    • 只描述业务逻辑, 不描述交互方式
  • 请求/响应模型

    • 输入输出都是简单的数据结构
    • 不派生任何 HTTP 接口和用户界面细节
    • 避免引用业务实体, 即使二者有很多相同的数据
    • 因为两个对象会以不同原因和速率发生变更
    • 会违反共同闭包原则(CCP)和单一职责原则(SRP)

第 21 章 尖叫的软件架构

  • 架构设计的主题应该是业务, 而非架构/框架本身

    • 推荐阅读《Object Oriented Software Engineering: A Use Case Driven Approach》
    • 系统的架构图基于用例, 而不是框架
  • 良好的架构

    • 围绕用例, 可以在脱离框架、工具和使用环境的情况下完整地描述用例.
    • 尽可能允许推迟和延后决定细节: 框架、数据库、Web 服务等. 并且容易改变
  • Web 只是一种交付手段, 而非架构

    • 系统应尽量保持它与交付方式之间的无关性
    • 应该可以将应用程序交付成命令行程序、Web 程序、富客户端程序、Web 服务程序等任何一种形式
  • 框架是工具而非信条

    • 待着怀疑的态度审视每一个框架
    • 权衡使用框架、保护系统
    • 保持对系统用例的关注,避免让框架主导架构设计
  • 可测试的架构设计

    • 架构设计应围绕用例展开, 应该可以在不依赖框架、Web 服务、数据库的情况下对用例进行单元测试

第 22 章 整洁架构

  • 几种架构

    • 六边形架构/端口与适配器架构
    • 《Growing Object Oriented Software with Tests》
    • DCI 架构
    • BEC 架构
    • 《Object Oriented Software Engineering: A Use Case Driven Approach》
  • 设计目标: 按照不同的关注点对软件进行切割
  • 特点

    • 独立于框架: 框架作为工具而非依赖
    • 可被测试: 脱离框架、数据库、Web 服务测试
    • 独立于 UI: UI 变更很容易
    • 独立于数据库: 轻易替换数据库
    • 独立于外部机构: 不依赖任何外部接口
  • 整洁架构

    • 分层: 中心为高层
    • 内层: 策略
    • 外层: 机制
    • 依赖关系: 由外指向内
    • 业务实体: 封装关键业务逻辑, 可以是带有方法的对象或一组数据结构和函数的集合
    • 用例: 特定应用场景下的业务逻辑, 封装了系统的所有用例, 引导业务实体的数据流
    • 接口适配器: 一组数据转换器, 负责在内部(用例和业务实体)和外部(数据库、Web)之间进行数据转换, 本层内部的同心圆不依赖任何数据库
    • 框架与驱动程序: 包含所有实现细节(Web 和数据库等), 实现不影响内层, 只有一些与内层沟通的黏合性代码
    • 跨越边界: 如图像右下角, 控制流从控制器开始, 穿过用例, 最后执行展示器代码
    • 当用例代码需要调用展示器时, 不能违反依赖关系直接调用, 可以使用依赖反转原则(DIP)来解决.
    • 跨越边界的数据: 数据结构应独立、简单, 避免直接传递业务实体或数据库记录对象

第 23 章 展示器和谦卑对象

  • 谦卑对象模式: 按照是否难以测试将行为拆分成两组模块, 其中包含系统中所有难以测试的行为的一组模块称为谦卑(Humble)组.
  • 谦卑对象模式应用

    • 展示器与视图
    • GUI 难以进行单元测试, 但可以利用谦卑对象模式将 GUI 拆分成展示器与视图两部分
    • 视图: 难以测试的谦卑对象, 代码越简单越好, 只负责将数据填充到 GUI 而不做任何处理
    • 展示器: 可测试的对象, 负责接收和处理数据, 以便视图将其呈现在屏幕上
    • 数据库网关
    • 用例交互器与数据库中间的组件, 是一个多态接口, 包含了应用程序在数据库上要执行的所有操作
    • SQL 不应出现在用例层代码中, 需要由数据库网关接口提供, 其实现由数据库层来负责, 这些实现(SQL 或其他数据库提供的接口)属于谦卑对象
    • 数据映射器

      • ORM(对象关系映射器)只是将数据从关系数据库加载到了对应的数据结构中, 属于数据库层, 是在数据库和数据库网关接口之间构建了一种谦卑对象的边界
    • 服务监听器: 从服务接口中接收并处理数据, 使得数据可以跨服务边界传输. 也属于谦卑对象模式
  • 谦卑对象模式将最难以测试的跨边界的数据交互行为分割出来, 可以大幅提高整个系统的可测试性.

第 24 章 不完全边界

  • 架构边界的挑战—— 引入不完全边界的原因

    • 构建完整的架构边界成本极高: 设计双向多态边界接口、输入输出数据结构、依赖关系管理、分割组件等
    • 为了应对将来可能的需要, 希望预留边界
    • 违背YAGNI原则(You aren't going to need it, 不要预测未来的需要)
  • 构建不完全边界

    • 省掉最后一步: 将系统分割为可独立编译、部属的组件之后, 再将其合并起来构建成一个组件
    • 设计工作量和代码量与构建完整边界相同
    • 省去了多组件发布管理的工作
    • 危险性: 组件之间的独立性逐渐降低、隔离弱化
    • 单向边界: 在设计时就进行必要的依赖反转, 使得跨边界调用保持单向
    • 危险性: 只能依赖于开发者和架构师的自律性来保证组件的持久隔离
    • 门户模式: 边界由一个统一的类来定义, 这个类中包含了所有的服务函数列表, 负责将外层的调用传递给外层不可见的服务函数
    • 危险性: 外层组件传递性地依赖于所有服务函数

第 25 章 层次与边界

  • 本章示例: 将一个简单的小程序逐步扩展为具有系统架构边界的复杂程序
  • 架构边界可以存在于任何地方, 需要小心审视何时需要设计架构边界
  • 困难之处

    • 完全实现边界需要很高的成本, 且违反 YAGNI 原则, 容易过度设计
    • 如果事先忽略了某些边界, 后续再添加可能极为困难
  • 架构师

    • 权衡哪里需要设计架构边界
    • 权衡需要完整边界 or 不完整的边界
    • 持续观察系统演进、权衡架构边界成本

第 26 章 Main 组件

  • 负责创建、协调、监督其他组件运转
  • 最底层、最细节的策略, 没有其他组件依赖于它
  • 任务

    • 设置起始状态、配置信息、加载外部资源, 并将系统控制权交给最高抽象层的代码
  • 可以以插件形式为系统设计多个 Main 组件对应于不同的配置

第 27 章 服务: 宏观与微观

  • 面向服务的架构

    • 服务只是一种跨进程/平台边界的函数调用, 不一定蕴含架构的意义
    • 架构是由跨越架构边界的关键函数调用来定义的, 并且必须遵守依赖关系规则
  • 服务的好处谬论

    • 解耦合
    • 任何形式的共享数据行为都会导致强耦合
    • 服务的接口与函数接口类似, 并没有更好
    • 独立开发部属
    • 并非服务仅有的特性, 采用单体或组件模式同样可以独立开发和部属
    • 强耦合的服务并不能真正做到独立开发部属维护
  • 服务与架构

    • 系统的架构边界在服务内部的组件而不在服务上
    • 在服务内部应采用遵守依赖关系原则的组件设计方式

第 28 章 测试边界

  • 测试也是一种系统组件

    • 遵守依赖关系原则: 处于最外层, 向内依赖
    • 测试组件可以独立部署(测试环境)
    • 支持开发过程, 而非运行过程(往往不会部属到生产环境)
  • 可测试性设计

    • 脆弱的测试问题
    • 测试代码与系统强耦合, 系统组件的小变化都需要测试组件做出相应变更
    • GUI 是多变的, 通过 GUI 来验证系统的测试一定是脆弱的, 应该让业务逻辑不通过 GUI 也能被测试
    • 测试专用 API
    • 拥有超级用户权限, 允许测试代码忽视安全限制、绕过成本高昂的资源(数据库), 强制将系统设置到可测试状态中
    • 将测试代码从应用程序中分离
    • 避免结构性耦合

      • 如果每个产品函数都有一个对应的测试函数, 那么测试套件与应用程序在结构上是紧耦合的, 导致脆弱的测试问题. 测试专用 API 使测试代码与应用程序解耦.
    • 安全性: 具有超级权限的测试专用 API 应该放置在单独的、可独立部属的组件中

第 29 章 整洁的嵌入式结构

  • 软件与固件

    • 软件本身不会随时间而磨损, 周期很长
    • 但硬件和固件会随着硬件演进而过时, 进而可能导致软件无法使用.
    • 固件代码
    • 对特定硬件平台 API 依赖的代码都属于固件代码, 如未分离业务与系统 API 调用的 Android 开发
  • 分离固件代码: 延长软件代码的生命周期
  • ”程序适用测试“

    • 如果代码只有在特定硬件平台上才能被测试, 那么即使通过了“适用性测试”, 仍不能说其拥有整洁的嵌入式架构. 除非这个产品永远不需要迁移到其他硬件平台
    • 目标硬件瓶颈: 嵌入式开发面临的特有的问题
  • 整洁的嵌入式架构就是可测试的嵌入式架构
  • 目标硬件瓶颈解决方案

    • 分层
    • 硬件、固件、[操作系统]、软件
    • 边界

      • 代码与硬件: 边界比较清晰
      • 软件(操作系统)与固件
      • 硬件抽象层(HAL): 为软件提供服务, 隐藏硬件实现细节
      • 软件与操作系统
      • 操作系统抽象层(OSAL): 隐藏操作系统实现细节
    • 面向接口编程与可替代性
    • 模块之间定义接口进行通信
    • 每一个借口都为平台之外的测试提供替换点
    • DRY 条件性编译命令
    • 问题: 如果程序中多次使用了重复的条件性编译命令来为不同平台启用/禁用一段代码
    • 方案: 使用硬件抽象层(HAL)隐藏硬件类型, 然后使用链接器或某种运行时加载器进行软硬件组合

第六部分 实现细节

第 30 章 数据库只是实现细节

  • 数据库不是数据模型, 而只是存储数据的工具
  • 依赖数据库表结构的代码应该被局限在系统架构的最外层的工具函数中
  • 数据库溯源: 优化磁盘存储

    • 文件系统: 便于存储和检索文档, 但对文档内容难以关注
    • 数据库系统: 关注文档/记录的内容/属性
    • 存储的未来
    • 基于 RAM, 将数据组织成最合适的数据结构
    • 基于文件和表格(数据库)的形式被逐渐取代
  • 系统架构不应关心数据在磁盘上如何存储这种实现细节
  • 性能考量

    • 性能是系统架构的一个考量标准
    • 数据存储方面的性能是底层问题, 不需要与系统架构相关联

第 31 章 Web 是实现细节

  • Web 的振荡式发展

      1. 将计算资源集中在服务器集群中, 浏览器保持简单
      1. Web2.0 用 Ajax 和 JavaScript 将很多计算挪到浏览器中执行
      1. 用 Nodejs 技术将 JavaScript 代码挪回到服务器中执行
  • GUI 只是实现细节, 而 Web 是 GUI 中的一种
  • 作为软件架构师, 需要将其与核心业务逻辑进行隔离
  • 将 Web 应用抽象为设备无关架构

    • 业务逻辑 -> 一组用例
    • 用例 -> 输入、处理、输出数据

第 32 章 应用程序框架是实现细节

  • 框架的目的

    • 解决框架作者所侧重的一些问题, 而不是解决你的问题——只是这些问题有较大的重合性
  • 单向约定

    • 开发者需要遵守框架的一系列约定
    • 框架作者不需要遵守什么约定
  • 风险

    • 框架自身的架构设计可能不正确
    • 产品的演进可能超出框架提供的能力范围
    • 框架本身可能朝着我们不需要的方向演进, 被迫进行不必要的升级或悄悄改变了行为
  • 解决方案

    • 将框架作为实现细节, 不要将其引入内圈
    • 不要基于框架的基类创建派生类, 可以创造一些代理类作为业务逻辑的插件
    • 根据依赖关系原则, 将框架作为核心代码的插件
    • 可以在最外层的 Main 组件中引入、依赖框架
  • 对于不得不接受的框架依赖, 需要慎重决定

第 33 章 案例分析: 视频销售网站

    1. 识别系统中的各种角色和用例
    2. 角色: 单一职责原则(SRP)
    3. 角色作为系统变更的主要驱动力, 一个角色的变更需求不影响其他角色
    1. 构造组件架构图
    2. 构建系统架构边界
    3. 分割组件
    4. 每个组件对应一个潜在的独立部署文件, 包含视图、展示器、交互器、控制器文件
    5. 独立部属

      • 每个组件独立交付部属是否过于繁琐?
      • 将组件组合为多个交付单元来部属, 如交付为视图、展示器、交互器、控制器和工具类 5 个.jar 文件
    1. 依赖关系管理
    2. 控制流: 从控制器输入数据, 经由交互器处理, 再由展示器格式化出结果, 最后由视图展示结果
    3. 依赖关系: 与控制流方向相反, 由低层指向高层
    4. 使用关系: 与控制流一致
    5. “继承”关系: 与控制流相反
  • 总结: 架构实现了两个维度上的隔离

    • 根据单一职责原则隔离各个角色
    • 应用依赖关系原则

第 34 章 拾遗

  • 代码结构设计

    • 水平分层
    • 层次

      • Web 代码
      • 业务逻辑
      • 持久化
    • 在项目初期合适, 不会过于复杂; 一旦软件规模扩展, 就需要进一步进行模块化
    • 问题: 无法展现具体的业务领域信息
    • 垂直切分
    • 按功能、业务概念或聚合根(DDD 术语)切分, 每一类放在一个包中, 以业务概念命名
    • 水平分层与垂直切分都很不好
    • 隔离业务领域与实现细节(数据库、框架等)
    • 端口和适配器模式
    • 六边形架构
    • 边界、控制器、实体
    • 按组件封装
    • 将一个粗粒度组件相关的所有类放入一个包, 类似于微服务架构, 将 UI 与粗粒度组件分离
    • 新的组件定义: 在一个执行环境(应用程序)中的、一个干净、良好的接口背后的一系列相关功能的集合
    • 优点: 一类业务的变更只需要修改一个粗粒度组件
    • C4 软件架构模型

      • 系统由一个或多个容器组成(Web 应用、移动 App、数据库、独立应用、文件系统等)
      • 容器包含一个或多个组件
      • 组件包含一个或多个类
    • 将代码分散到不同的代码树
    • 如端口与适配器架构

      • 例 1
      • 业务代码树: 所有技术和框架无关的代码
      • Web 源代码树
      • 持久化源代码树
      • 例 2
      • 业务(Domain)代码(内部)
      • 基础设施(Infrastructure)代码(外部)
  • 中心思想: 要将架构设计映射到具体的代码结构上

思维导图

Clean Architecture 思维导图


Profile picture

佚树 的个人博客

关于前端、音乐与生活