---
title: 《架构整洁之道》导读
category: 读书笔记
---

架构整洁之道，实乃省钱之道。**省钱**，是读懂本书的一个核心视角。专业人士的价值，不仅在于交付满足当下需求、乃至未来变化的软件，更在于他们能够采用**效率最高、成本最低**的软件架构完成此事。这本书就是写给预备承担、或者正在承担架构责任的资深工程师们。

本篇中，我不仅会分享我觉得本书精要的内容，更主要的是我会分享我的读书方法。读书的顺序，有时跟目录顺序并不是完全一致的，如何按照自己的方式抓住全书脉络并安排阅读顺序，是我觉得也很有趣、值得分享出来互相交流的一部分。我一直认为，内容与形式是统一的，形式正是内容的一部分。不注意表达的形式，就难以真正了解被表达的内容。

## 目录

* [全书结构](#全书结构)
* [什么是软件架构](#什么是软件架构)
* [什么是组件](#什么是组件)
* [软件架构的研究内容](#软件架构的研究内容)
  * [如何划分组件：聚合原则](#如何划分组件：聚合原则)
  * [避免反模式：组件耦合](#避免反模式：组件耦合)
  * [如何管理依赖关系](#如何管理依赖关系)
  * [如何管理边界/通信方式](#如何管理边界/通信方式)
* [软件架构的案例及分析](#软件架构的案例及分析)
* [结语](#结语)

## 全书结构

全书共34章，其中原书第2-5部分（第3-29章）占比最多达79%，是全书的主要部分。第1部分开宗明义，讲清了软件的架构价值是什么，为本书所讨论的话题定下框架；第2、3、4部分由浅入深，从低层到高层依次探讨了软件架构的构成元素：编程范式、设计原则和组件。第5部分开始，以组件为基本元素展开对软件架构的讨论；第6部分软件架构核心以外的实现细节。

本书的主题是**软件架构**，而组成架构的基本元素是**组件**，因此阅读本书的**第一个关键，首先是弄清这“软件架构”及“组件”这两个词的内涵**，它们相关的章节（第1-2、15章讲架构，第12章讲组件）是本书的核心，为了弄清楚我甚至翻阅了原文。基于清晰的组件定义，我们得以进一步讨论软件架构，它涉及对系统进行组件划分（第13-14章）、管理组件之间在编译期的依赖关系（第16章、第19章、第24章）以及运行时的边界/通信方式（第17-18章）。最后，作者在22章给出了这样一个符合所有原则的“整洁架构”，并在第6部分的末尾给出了两个分析实例（第33-34章）。

所以本书的阅读脉络是这样：

1. 什么是**系统架构**（第1-2章、第15章）
2. 什么是构成架构基本要素的**组件**（第12章、第13-14章）
3. 软件架构研究的核心内容（第16-19章、第24章）
4. 软件架构案例、设计思路与分析（第22章、第33-34章）

其他的部分，固然不乏精彩的洞见（比如面向对象的本质是基于多态能力对源码级依赖进行管控的架构意义上的能力、SRP中指出系统的行为者actor是导致软件修改的原因，等），但我的读书过程仅集中在“软件架构”和“组件”这两条主线上，因而我会弱化这些部分的讨论、留给读者自行阅读。这些被弱化的章节包括：第2-3部分（编程范式、设计原则）、第5部分部分章节（第20-21、22-23、25-26、28-29章，读者可按需阅读）以及第6部分的部分实现细节章节（第30-32章，一句话可以说完：数据库、UI、框架都应该依赖于业务逻辑组件作为其插件，以免污染核心架构）。

## 什么是软件架构

这是本书第1部分探讨的问题。

> 软件架构的终极目标是，用最小的人力成本来满足构建和维护该系统的需求。 —— P5
>
> 软件架构的终极目标就是最大化程序员的生产力，同时最小化系统的总运营成本。 —— P122

看看，什么叫做专业~~省钱~~。专业人士对于其雇主和他人的最大价值，不仅在于他能使软件具备长时间内响应业务需求的能力（系统的行为价值），而且在于他能用最低的成本来完成这件事（系统的架构价值）。这包括内置的高质量、各种自动化部署、减少软件过程各种不必要的架构浪费等，一言以蔽之，软件架构是一件专业的、精益的工作，它应该交由专业人士来完成。

这里作者又敏锐地指出，这个做架构的专业人士（可以称之为架构师），仅仅是一个角色（通常是编码最优秀的那个工程师承担），而不是一个脱离编码一线的、隔离的“岗位”。那些认为“架构”属于“高层级”工作，而“编码/设计”属于“低层级”的实现型细节型工作，“架构师”的工作就是发号施令指挥“（可替换的）程序员”去干活的想法，本身就是不专业的、错误的（如果不是坏的）。不涉及实现细节的架构工作是不存在的。架构设计必须由奋斗在一线的、经常接触细节的、最有想法的、有经验的、更加资深的、富有责任感的工程师来担当，连代码都不写/写不好的大佛爷们可以走开了，斗争的就是你。

**减少成本**（各种不是必要的成本），这是看待软件架构一切元素的根本出发点，也是更好使用本书的一个视角。

那么具体来说，有哪些方面的成本可以减少呢？达成此目标的策略是什么呢？策略之下有哪些具体的办法/手段可以达成“最小化成本”这个目标呢？作者在第5部分“软件架构”的开篇——第15章——详细地回答了这几个问题，可谓是纲举目张。这部分总纲，之所以没放到第1部分来，我认为是因为软件架构的基本元素涉及一个“组件”的概念，而为了讲解组件这个概念，作者又花了3个部分、12个章节、超过三分之一~~specifically, 12/34≈35%>33%，我是很严谨的~~的篇幅来铺垫，所以不得不把15章放到第5部分去。但是作为读者，我们完全可以暂时把“组件”的具体概念束之高阁，来看看15章关于**软件架构**的总纲。

15章集中指出：

**软件架构的终极目标及评价标准**，是最大化程序员的生产力，同时最小化系统的总运营成本。

**软件架构的主要目标**，是为了支撑软件系统的全生命周期，减少软件过程中（对组成系统架构的各个组件的）研发、运行、部署、维护等方面的成本。

**软件架构的主要策略**——也即实现该主要目标的策略——是在设计中尽可能长时间地保留尽可能多的可选项。

**软件架构的实现办法**——也即其研究对象/工作内容——涉及规划如何将系统合理切分成为组件（第12-14章）、安排好组件之间在编译期/源码级别的依赖关系（第19章），以及设计组件之间在运行时的边界和通信方式（第16-18章）。

弄清楚软件架构的根本目标、方法论及研究对象，本书的主要内容和结构就无比清晰地呈现在了我们面前。接下来，我们需要研究本书第二个关键的概念，也就是软件架构的主要研究对象——组件。

## 什么是组件

[中文版译本中对“组件”的定义与原意有些小出入][架构整洁之道导读（二）续]，这个时候我们可以直接去阅读原文。我把[O'Reilly上原书对组件定义的前两段][12 - Components]摘录如下并做了强调：

> **Components are the units of deployment. They are the smallest entities that can be deployed as part of a system**. In Java, they are jar files. In Ruby, they are gem files. In .Net, they are DLLs. In compiled languages, they are aggregations of binary files. In interpreted languages, they are aggregations of source files. In all languages, they are the granule of deployment.
> 
> Components can be linked together into a single executable. Or they can be aggregated together into a single archive, such as a .war file. Or they can be independently deployed as separate dynamically loaded plugins, such as .jar or .dll or .exe files. **Regardless of how they are eventually deployed, well-designed components always retain the ability to be independently deployable** and, therefore, independently developable.

这两段指出了作者对“组件”的定义，具备**物理级别的部署单元**和**源码级别的逻辑单元**两个层次。建立这个区分极其重要，它将直接影响你阅读后续章节的流畅性，在全书的行文中作者提到的“组件”有时主要指物理意义上的“部署单元”（比如13-14章讨论组件聚合和耦合时），有时又主要指源码意义上的“逻辑单元”（比如第22章P183页的图22.2显然不能理解为应用的每个分层都是物理意义上的部署单元，我曾经一度陷入混乱和自我怀疑之中）。

组件作为物理意义上的部署单元，这层含义应该很明确，因为作者说“组件是软件的部署单元，是可以作为系统的一部分被部署的最小实体”。那么部署单元又是指什么？这两段举例了，对于编译型语言来说，它可以是一组二进制文件的集合（比如.jar文件、DLL文件等）；对于解释型语言来说，它直接就是一组源代码文件的集合。通读12章后续“组件发展史”等几小节后，你会发现作者所说的部署单元，就是指经过“编译”（compile）、“链接”（link）等阶段（解释型语言可能不存在这些阶段）之后得到的产物：一个可以被加载器（loader）直接加载（load）并执行（execute）的文件。我的理解，说白了，**组件就是一个可执行文件**。它是一个部署概念。

再来看组件作为源码意义上的逻辑单元这层含义。原文讲，“不管组件**最终**被如何部署（采用何种部署形式），设计良好的组件应该始终保有可被独立部署的能力”。也即是说，哪怕是一个尚未成为物理形式的可独立部署单元、但在设计随时保有这种可能性的组件——也就是源码级别的一组文件集合——仍然可以称为组件。这肯定了组件在作为部署单元的物理意义之外的另一种可能形式，也即仅仅存在于源代码级别的逻辑单元。这是一个逻辑概念。

由此，我们明白了，作者对组件具备两个层次的定义：其主要定义是物理意义上的部署单元，此外还有尚未成为物理部署单元、但随时保有此种向物理部署转化能力的、源码意义上的逻辑单元。

拆分出独立可部署的组件，其意义在于复用、减少成本（别忘记软件架构的终极目标）。但拆分又会带来部署上的成本（包括但不仅限于基础设施如VCS/JIRA/Artifactory/CI/CD、测试、部署、发布管理等成本）。那么，如何知道拆（出物理上可部署）组件是不是系统上省钱的方案呢，就需要知道什么是好的组件（组件特征或构成原则），以及什么是健康的组件间关系。这就是第13、14章的内容。

同时还有一个问题留给读者：源代码级别的组件拆分并不涉及部署上的种种事宜（和成本），那么做这个层面的组件拆分又有什么意义呢？

## 软件架构的研究内容

### 如何划分组件：聚合原则

第13章讲组件聚合，回答了哪些类应该被放到同一个组件里的问题，给出了三条原则，也给出了这三条原则之间的相关关系（张力图）。这三条原则分别是复用/发布等同原则（REP）、共同闭包原则（CCP）及共同复用原则（CRP）。

REP原则，可以参考[这篇导读][架构整洁之道导读（二）续]给出的[这个链接][Granularity]，文章指出“复用”的核心问题在于所有权。所谓复用（reuse）跟复制粘贴（copy-paste）虽然都能实现代码意义上的重用，但是“复用”别人的代码，你不需要了解实现细节、不需要自己维护、你可以收到作者的更新，可以决定是否/何时升级。这就是“复用/发布”等同的说法，复用的最小单位是带有版本标识的包（package），没有发布就没法复用。通过版本管理进行发布，是复用的基本要求。可以认为，在现代包管理工具已经极大普及的今天（如Gradle/Maven/NPM/RPM/Gems等），REP原则一般默认得到了满足。

CCP原则指的是，经常一起复用、一起修改（变更来源/动力相似）的类应该放到同一个组件中。否则，要进行一次修改，可能就要修改很多组件、做多次发布、要求用户做多次联动升级。

CRP原则指的是，不要强迫用户依赖他们不需要的东西。一个包C发出去A和B在用，但是A只关心C1功能，B只关心C2功能，这不好，用户可能经常收到他不需要的更新通知。如果用户选择更新，还需要付出重新编译、测试、部署的成本，对于用户这本来是可以避免的。

具体实践时，我们一般会站住REP原则（也就是通过版本化的方式发布可复用包），在CCP和CRP中取舍。违反CCP，组件作者要付出“过多组件变更和发布”的成本，组件用户要付出“同时更新”的成本；违反CRP，用户可能需要关注无关更新、做不必要的升级。

我认为13章讲的组件聚合原则，是针对部署意义上的组件讲的。这些原则对于仍然在源码级别的组件来说是否适用，读者不妨自行判断。

### 避免反模式：组件耦合

14章讲的组件耦合，侧重于组件间的关系，但也部分属于不恰当放置类与模块带来的结果。14章讲了三个主要的原则：无依赖环原则（ADP）、稳定依赖原则（SDP）及稳定抽象原则（SAP）。

ADP那节举的“一觉醒来综合征”这个场景，[是发生在源代码级别的、多人协作的大项目背景下][Granularity]。这里讲的又不是部署意义上的组件了，see，interesting，这本书在涉及“组件”这个概念时其实是在部署和源码两种场景下不断切换的。解决方案是，把项目拆分成一系列组件，每个组件都上版本，通过版本互相引用，并且不要循环依赖，方法是应用DIP或拆新的组件。否则，一觉醒来还是不安宁，构建、测试、部署过程还是需要协调。

SDP讲的是，依赖需要指向更稳定的方向。何谓稳定？作者指出，简化模型，只看依赖关系。被许多用户依赖的组件是稳定的，而过多依赖于其他组件的组件是不稳定的。至于那套计算稳定性I的量化指标，看看就好，用不上。

SAP指出，组件的抽象化程度应该与稳定性保持一致。也即是说，组件越稳定，就应该越抽象，因为抽象的策略往往在高层，不容易变更；而组件越不稳定，就应该越具体，应该变更起来更容易。同样，那套衡量抽象化程度A的量化指标，也是~~一言难尽~~……不用看了。~~没人会去算的，算了也不能拿来指导行动的。~~

### 如何管理依赖关系

前面谈到，实现软件架构的主要策略是**保留可选项**。保留可选项的一个方法，就是将系统划分为一些隔离良好的组件，以便隔离某些组件的变化对整体软件系统带来的影响。这样，我们就可以推迟某些与核心业务逻辑无关的技术决策（比如UI、框架、数据库、工具库等）。

16章指出，这样的隔离可以在用例和分层两方面展开，以实现层间、用例间互相独立。按层解耦，一个系统一般可以被解耦成若干个水平分层：UI界面、业务逻辑、数据库；按用例解耦，则是垂直方向的解耦，每个用例都可以跨越若干分层。注意，解耦可以发生在不同层面，按层解耦也未明确层间实际的依赖关系。

19章指出，低层组件（不稳定的、具体的细节）应被设计为依赖于高层组件（稳定的、抽象的策略）。也就是说，分层中的UI和数据库都应该依赖于业务逻辑，因为前两者不属于核心部分，不稳定。如果高层组件需要使用低层组件提供的服务（比如业务逻辑调用数据库），最常见的办法是通过依赖反转原则（DIP）创建出一个接口并把它放在高层组件中，让低层组件来实现接口，以此实现隔离。

16章指出，按层和用例解耦一个系统有很多方式。例如，它至少可以发生在三个层次上：**源码层次**、**二进制层次（部署）**、**执行单元层次（服务）**。

* **源码层次**：组件及其依赖关系解耦仅发生在源码层，可以隔离组件间的重新编译，但部署上仍是一个执行单元，此模式也称单体结构
* **部署层次**：组件依赖关系的解耦发生在部署单元间，可以隔离组件间的重新编译及部署，部署上一般仍位于同个处理器及地址空间内
* **服务层次**：组件之间仅仅通过数据结构进行耦合，可以隔离组件间的重新编译及部署，部署上不假设服务之间位于同个处理器，它们仅通过网络通信

作者建议，**解耦可以从最简单的源码层次做起（也即单体），后续根据需要演进成部署单元、服务，并保留回退成源码、部署单元的可能性。**

在处理依赖关系、反转依赖的同时，24章又承许完全边界需要耗费成本去构造和维护，因此适时使用不完全边界来降低成本、并保持向完全边界转化或退化的能力是很可行的。常见的不完全边界构造方式24章介绍了三种：构造完整边界但省去最终部署（也即是源码意义上的逻辑组件）、单向边界、门户模式（连接口都不创建）。

我有个问题，对于一个6-8人的敏捷团队来说，他们维护的每一个项目，通常只会有为数不多的部署意义上的组件/服务，更多时间，组件是以逻辑单元的形式存在于每一个组件/单体中，并且组件之间的边界也很有可能是以不完全边界的形式存在（因为向完全边界转换也就是一项重构手法的距离，完全可以在需要的时候再做），那么这种情况下，本书研究的组件拆分、边界维护对于源码级别的组件还有什么意义吗？源码意义上的组件架构不就也只是模块级别的软件重构和整洁代码设计了么？说到底还是重构、整洁代码和TDD三板斧搞定大多数软件设计？

### 如何管理边界/通信方式

介绍完了组件的拆分原则、解耦方法与编译期的依赖关系设计原则后，软件架构的最后一部分内容与边界有关。是边界将不同的组件分割开。跨边界调用，表现为“边界一侧函数调用另一侧函数，并互相传递数据”的行为，它是一个**运行时**的概念。边界有多种不同的表现形式，16章介绍的几种解耦方式，可以被设计成不同的边界形式（即通信方式），18章介绍了常见的4种边界形式：**单体（monolith）边界、部署组件（deployment components）边界、本地进程边界、服务边界**。

| 边界类型 | 物理形式 | 边界级别 | 运行时 | 开销 | 通信频率 | 
| :--- | :---: | :---: | :--- | :--- | :---: |
| 单体边界 | 无 | 源码级别 | 作为同个执行文件，位于同一处理器和地址空间 | 基本无 | 频繁| 
| 部署组件边界 | 库（常见） | 部署级别 | 有一次性编译/链接成本，仍位于同一处理器和地址空间 | 基本无 | 频繁 | 
| 本地进程边界 | 进程 | 进程级别 | 运行时不是同个执行文件，一般不共享地址空间，通过socket或系统机制（如mailbox、message queue）等通信 | 系统调用、数据编码解码、进程上下文切换等 | 较低 |
| 服务边界 | 服务（常见）| 网络级别 | 运行时不是同个执行文件，不假设位于同个处理器或地址空间，通过网络通信 | 网络延迟等 | 最低 |

组件的解耦模式（源码级别或部署级别）与其边界实际存在的物理形式之间关系如下：

|   解耦模式   | 可部署的运行时边界形式                                   |
| :----------: | :------------------------------------------------------- |
| 源码层次解耦 | 一般以单体边界形式存在                                   |
| 部署层次解耦 | 一般以部署组件边界形式存在                               |
| 服务层次解耦 | 一般以服务边界形式存在，也可能部署成为本地进程边界的形式 |

## 软件架构的案例及分析

在22章，作者给出了一个架构方案，综合了前面提到的几个关键性的架构问题，包括组件拆分、解耦模式、依赖关系设计等。其中，最关键的一点是它要遵循19章所提到的依赖原则：依赖永远由低层组件指向高层组件。

![Clean Architecture](https://blog.cleancoder.com/uncle-bob/images/2012-08-13-the-clean-architecture/CleanArchitecture.jpg)

34章是关于分层架构如何组织依赖关系的佳作（Simon Brown同时也是[程序员必读之软件架构][]一书的作者），强烈建议仔细阅读。

## 结语

待写。

[架构整洁之道导读（二）续]: https://www.jianshu.com/p/fdb8c8a604b1
[架构整洁之道导读（三）]: https://www.jianshu.com/p/d651c9fb1755
[12 - Components]: https://www.oreilly.com/library/view/clean-architecture-a/9780134494272/ch12.xhtml#ch12
[Granularity]: http://condor.depaul.edu/dmumaugh/OOT/Design-Principles/granularity.pdf
[程序员必读之软件架构]: https://www.douban.com/search?q=%E7%A8%8B%E5%BA%8F%E5%91%98%E5%BF%85%E8%AF%BB%E4%B9%8B%E8%BD%AF%E4%BB%B6%E6%9E%B6%E6%9E%84
