《架构整洁之道》详解:从编程范式到整洁架构的完整指南
- 约 25 分钟读完 - 全文 9666 字《架构整洁之道》详解:从编程范式到整洁架构的完整指南
《架构整洁之道》(Clean Architecture: A Craftsman’s Guide to Software Structure and Design)是 Robert C. Martin(业内昵称"Bob 大叔")在 2017 年出版的一本书。它是《代码整洁之道》《敏捷软件开发》之后,Bob 大叔对自己半个世纪编程经验的又一次系统总结。
这本书要回答的核心问题只有一个:
如何设计一个系统,让它活得久、改得动、换得起?
本文面向从未接触过软件架构概念的读者,按照全书六个部分的结构逐层展开,配合图示和实例,帮你建立完整的知识地图。
第一部分 概述:架构到底是什么
1. 设计与架构没有本质区别
很多人以为"架构"是宏观的大图,“设计"是微观的细节。Bob 大叔认为两者其实是一回事:架构就是系统设计,系统设计就是架构。它们只是在不同的观察尺度上,不存在一条清晰的分界线,两者共同决定了系统的整体形状。
一个好的架构目标可以用一句话概括:
用最小的人力成本来满足构建和维护该系统的需求。
衡量标准很朴素:如果每次新增需求所需的成本都维持在较低水平,不会因为系统变老而持续上升,那这个架构就是好架构。反之,如果每改一个功能都要牵动全身、修复一个 bug 引入三个新 bug,那架构就已经坏掉了。
2. 两个价值维度:行为与架构
每个软件系统都为利益相关者提供两种不同的价值:
- 行为价值(Behavior):系统按需求文档干活,帮用户赚钱或省钱。这是程序员最关注的部分。
- 架构价值(Architecture):软件之所以叫"软"件,就在于它应该是容易被修改的。如果需求变了而系统改不动,这个系统就失败了。
这两者的优先级,可以用艾森豪威尔矩阵(重要/紧急四象限)来思考:
| 紧急 | 不紧急 | |
|---|---|---|
| 重要 | ① 行为(功能) | ② 架构 |
| 不重要 | ③ 界面细节等 | ④ 杂项 |
常见的悲剧是:业务经理永远把"紧急"排在前面,于是团队不断牺牲"重要但不紧急"的架构,最终系统腐化到无法维护。为好的架构而斗争,是软件架构师的职责——因为架构师本身就是利益相关者之一。
第二部分 从基础构件开始:编程范式
这一部分回顾了软件历史上仅有的三种编程范式。一个很有意思的观点是:
这三种范式都是 1958–1968 这十年间被发明的,之后的几十年里,再也没有出现过新的编程范式。
三种范式:它们都在"限制"我们
三种范式有一个共性:它们都剥夺了程序员的某种能力,而不是赋予新能力。
graph LR
A[结构化编程<br/>1968 Dijkstra] -->|限制了| A1[goto 的无约束跳转]
B[面向对象编程<br/>1966 Simula] -->|限制了| B1[函数指针的滥用<br/>通过多态接管间接调用]
C[函数式编程<br/>1958 λ演算/Lisp] -->|限制了| C1[赋值语句<br/>变量不可变性]
-
结构化编程(Structured Programming):用顺序、选择、循环三种结构取代了自由的
goto跳转。Dijkstra 的名言是"goto 是有害的”。这一范式还带来一个深远影响:既然无法数学式地"证明"程序正确,那就通过测试来"证伪"——结构化编程催生了测试驱动开发(TDD)的思想土壤。 -
面向对象编程(OOP):把数据和函数打包成对象。关于 OOP 的三大特性,书里有独到见解:
- 封装:其实 C 语言用头文件隐藏实现做得更好,OOP 反而弱化了它;
- 继承:C 程序员早就手动模拟过,OOP 只是让它更方便;
- 多态:这才是 OOP 真正的核心——它让源码依赖方向可以被反转。调用方通过接口调用实现方,源码依赖与控制流方向相反,这就是全书后面反复强调的"依赖反转"的武器。
-
函数式编程(Functional Programming):核心思想是变量不可变(immutability)。没有赋值,就没有竞态条件、死锁、并发更新问题——所有并发 bug 都源于可变状态。现实中的妥协方案是"事件溯源"(Event Sourcing):只存事务日志,不存状态,需要状态时用日志重算,从而消灭对可变状态的依赖。
给初学者的 takeaway:软件架构的底层构件只有三种——顺序、分支、循环;而范式教会我们的是"不要做什么",架构师要在这些限制内搭建系统。
第三部分 设计原则:SOLID
SOLID 是函数/类级别(中层)的五个设计原则,目标是让软件模块:可扩展、易修改、易理解、可复用。这部分是全书中读者基础最扎实的部分,但书里对每条原则的理解都比市面上的通俗版本更精确。
SRP 单一职责原则
最容易被误解的一条。SRP 不是“每个函数只做一件事”(那是函数层面的小原则),而是:
一个模块应该只对一类行为者(actor)负责。
经典例子:一个 Employee 类同时有 calculatePay()(CFO 关心)、reportHours()(COO 关心)、save()(CTO 关心)三个方法。三个方法服务三类不同的行为者,一旦为 CFO 改的代码意外影响了 COO 的报表,就是 SRP 被破坏的恶果。解法是按行为者拆分。
OCP 开闭原则
对扩展开放,对修改关闭。
新增行为应该通过增加代码完成,而不是修改旧代码。实现手段是:把系统按"变更原因"分层,高层策略组件不受低层细节变化的影响,层与层之间用接口隔离。
LSP 里氏替换原则
任何使用基类的地方,替换成子类后行为依然正确。
违反 LSP 的著名案例:正方形(Square)继承长方形(Rectangle)——setWidth 语义在两个类里不一致,导致依赖长方形的代码出问题。在架构层面,LSP 延伸为"接口/服务的可替换性":REST 接口协议不一致,本质上也是违反 LSP。
ISP 接口隔离原则
不要强迫使用者依赖它们不需要的方法。
胖接口会引入不必要的依赖——改动接口中别人不用的部分,也会强制所有使用者重新编译部署。架构层面的教训:任何层次的"多余依赖"都有害,包括对一个打包了过多功能的库或框架的依赖。
DIP 依赖反转原则
源码依赖方向应与控制流方向相反,高层策略不应依赖低层细节,两者都应依赖抽象。
这是整本书的"元原则"。Java 这类语言中,接口声明属于调用方一侧(内层),实现类属于被调用方一侧(外层),依赖箭头跨过边界指向内层——这就是后面整洁架构能成立的技术基础。
graph LR
subgraph 内层["内层(高层策略)"]
UC[用例] -->|依赖接口| PORT["接口(输出端口)"]
end
subgraph 外层["外层(实现细节)"]
IMPL[Presenter 实现] -.实现.-> PORT
DB[数据库实现] -.实现.-> PORT2[数据访问接口]
end
UC -->|依赖接口| PORT2
style 内层 fill:#e8f0fe
style 外层 fill:#fef3e8
第四部分 组件构建原则
SOLID 管"类和函数怎么写",这一部分管"类和函数怎么打包成组件"(组件 = 独立部署的最小单元,如 jar、dll、服务)。
组件聚合三原则(哪些类该放进一个组件)
三条原则互相拉扯,架构师要做权衡:
- REP 复用/发布等同原则:组件的复用粒度等于发布粒度——一个组件里的东西应该作为一个整体被版本化和发布。
- CCP 共同闭包原则:会因同一原因、同一时间变化的类应该聚在同一个组件里(SRP 在组件层面的放大版)。
- CRP 共同复用原则:不要强迫组件的使用者依赖他们不需要的东西(ISP 在组件层面的放大版)。
CCP 和 CRP 通常是矛盾的:按变更原因聚合容易,但组件可能变胖;按使用方切得细,组件数量又会爆炸。
组件耦合三原则(组件之间怎么依赖)
- ADP 无环依赖原则:组件依赖图中不允许出现环。打破环的手段有两个:依赖反转(在中间加接口)或新建一个公共组件。
- SDP 稳定依赖原则:依赖方向必须指向更稳定的组件。越难修改的组件越稳定,不稳定(易变)的组件应该依赖稳定的组件。
- SAP 稳定抽象原则:越稳定的组件应该越抽象。所以架构应该是:核心策略 = 稳定 + 抽象(接口),外围实现 = 不稳定 + 具体。极端的两端是"最稳定且最抽象"(理想)和"最不稳定且最具体"(胶水代码),大部分组件落在两者之间的主序列附近。
第五部分 软件架构:本书的核心
1. 什么是软件架构
架构的实质是:尽可能长时间地推迟决策,保留尽可能多的可选项。
一个软件系统的架构质量,取决于它支撑系统全生命周期的能力:开发、部署、运行、维护。好的架构让系统易于理解、易于开发、易于维护、易于部署,其终极目标是最小化整个生命周期的人力成本。
架构的次要目标还包括让系统可以按用例独立开发、独立部署、独立运行。
2. 独立性
好架构必须支持:
- 按用例解耦:购物车、订单、用户管理……不同用例之间的代码互不干扰;
- 解耦运行方式:系统可以跑成单体、SOA、微服务,部署方式是可推迟的决策;
- 独立开发与独立部署:不同团队可以并行开发不同模块而互不阻塞。
3. 划分边界:软件架构就是画线的艺术
架构师的工作是画出边界线,把系统分割成组件。一部分组件实现核心业务逻辑,另一部分组件作为与核心逻辑无关的"插件"。
划线的依据是:哪些东西变化的原因不同。UI、数据库、业务规则、第三方服务,它们变化的理由完全不同,所以它们之间应该画线。
graph TD
UI[用户界面] -.插件式依赖.-> CORE
DB[(数据库)] -.插件式依赖.-> CORE
WEB[Web 框架] -.插件式依赖.-> CORE
EXT[外部服务] -.插件式依赖.-> CORE
subgraph CORE["核心业务逻辑(不知道外围的存在)"]
ENT[业务实体]
UC[用例]
end
style CORE fill:#e8f0fe
关键点:核心业务逻辑根本不依赖这些东西,是这些东西反过来依赖核心业务逻辑。数据库不是系统的中心,业务规则才是。这种"以核心业务为中心,外设皆插件"的思想,Bob 大叔称之为插件式架构。
4. 边界剖析:跨边界的调用怎么发生
跨越边界的通信分两种方向:
- 顺控制流调用(外层主动调用内层):直接调用即可,依赖方向天然向内,没有架构问题;
- 逆控制流回调(内层需要通知外层):必须用依赖反转——内层定义接口,外层实现接口,保证源码依赖依然指向内层。
5. 策略与层次
软件系统就是一种对"策略(policy)“的陈述:详细描述如何根据输入计算输出。
架构要做的就是把策略按层次分开:越靠近输入输出的策略层次越低(格式化、校验、IO),越远离的策略层次越高(业务规则)。高层策略与低层细节分离,低层策略应该成为高层策略的"插件”。
6. 业务逻辑:实体与用例
书里把业务逻辑分成两类:
- 业务实体(Entity / 关键业务逻辑):与具体应用无关的、企业级最核心的规则。比如"贷款利率 = 基准利率 + 风险系数",不管系统做成 Web、APP 还是命令行,这条规则都不变。
- 用例(Use Case / 应用相关逻辑):描述某个具体应用如何使用实体。比如"贷款审批流程:先查征信 → 再评估额度 → 最后出审批结果"。
用例依赖实体,实体不知道用例的存在。实体是最通用、最高层、最不容易变的;用例次之。
7. 尖叫的软件架构
这一章名字很有趣:你的架构应该"尖叫"着说出自己是做什么的。
看一个系统的目录结构,如果看到的是 models / views / controllers,这是框架在尖叫;如果看到的是 order / payment / shipping(订单、支付、物流),这是业务在尖叫。好的架构应该一眼看出这是一个"图书管理系统"或"电商系统",而不是"一个 Spring MVC 项目"。框架是工具,不应该喧宾夺主。
8. 整洁架构:四层同心圆(全书最著名的一张图)
Bob 大叔综合了三种前辈架构思想——六边形架构(Hexagonal / Ports and Adapters)、DCI、BCE——提炼出统一的模型:整洁架构(The Clean Architecture)。
这些架构的共同特点是系统:
- 独立于框架:框架是工具,不是牢笼;
- 可被测试:业务逻辑可以脱离 UI、数据库、Web 服务测试;
- 独立于 UI:Web 界面换成命令行界面,业务逻辑一行不用改;
- 独立于数据库:Oracle 换成 MongoDB,业务逻辑不知道;
- 独立于任何外部机构:业务逻辑对外部接口一无所知。
用 Mermaid 重绘经典的同心圆图(越靠内层,策略越高层、越抽象):
flowchart TD
subgraph L4["🟠 框架与驱动层 Frameworks & Drivers(最外层:实现细节)"]
direction LR
FW[Web 框架]
DB[(数据库)]
DEV[设备 / 外部接口]
end
subgraph L3["🟡 接口适配层 Interface Adapters"]
direction LR
CTRL[Controllers]
PRE[Presenters]
GATE[Gateways]
end
subgraph L2["🟢 用例层 Use Cases(应用业务逻辑)"]
UC[用例交互器<br/>Use Case Interactor]
end
subgraph L1["🔵 业务实体层 Entities(企业核心业务逻辑,最内层)"]
ENT[业务实体]
end
L4 -->|依赖指向内层| L3
L3 -->|依赖指向内层| L2
L2 -->|依赖指向内层| L1
贯穿整个架构的只有一条依赖关系规则(The Dependency Rule):
源码中的依赖关系必须只指向同心圆的内层,即由低层机制指向高层策略。
内层代码不能引用外层代码中声明的任何名字——函数、类、变量,一概不行。外层使用的数据格式(尤其是框架生成的)也不能被内层使用。
各层职责:
- 业务实体(Entities):封装全系统最关键、最通用的业务逻辑,是整个系统中最不容易受外界变化影响的部分。页面导航改版、安全策略调整,都不应该触及实体层。
- 用例(Use Cases):封装特定应用的业务逻辑,指挥数据在实体间流动,调度实体完成用例目标。用例的变化不影响实体,数据库/UI 的变化也不影响用例。
- 接口适配器(Interface Adapters):一组数据格式转换器。GUI 的整个 MVC 都在这一层:Controllers、Presenters、Views。所有 SQL 语句也应该被限制在这一层。
- 框架与驱动(Frameworks & Drivers):最外层,只写与内层沟通的胶水代码。Web 是细节,数据库是细节,都放在外层,“让它们伤不到内层”。
注意:四层不是硬性规定,真正的系统可以多于四层,但依赖关系规则永远成立。
9. 一个完整的请求流程(Web + 数据库的典型场景)
书中最实用的一张图描述了一个请求如何穿越各层。用"用户下单"为例:
sequenceDiagram
participant U as 用户
participant C as Controller<br/>(接口适配层)
participant IB as InputBoundary<br/>(输入端口接口, 用例层)
participant UCI as UseCaseInteractor<br/>(用例层)
participant E as Entities<br/>(实体层)
participant DAI as DataAccessInterface<br/>(数据访问接口, 用例层)
participant DB as 数据库实现<br/>(最外层)
participant OB as OutputBoundary<br/>(输出端口接口, 用例层)
participant P as Presenter<br/>(接口适配层)
participant V as View<br/>(最外层)
U->>C: 提交订单(HTTP 请求)
C->>IB: 包装成简单数据对象, 跨边界传入
IB->>UCI: 调用
UCI->>E: 调度实体执行业务规则
UCI->>DAI: 通过接口请求数据
DAI->>DB: 外层实现执行 SQL
DB-->>UCI: 返回简单数据结构
UCI->>E: 收集结果
UCI->>OB: 组装 OutputData, 通过输出端口传出
OB->>P: Presenter 实现该接口
P->>V: 格式化成 ViewModel(纯字符串/开关)
V-->>U: 渲染 HTML
三个要点:
- 控制流从 Controller 出发,穿过用例,最终到达 Presenter——方向是从外到内再到外;
- 源码依赖每一步都指向内层:Controller 依赖 InputBoundary 接口、Presenter 实现 OutputBoundary 接口,两者都属于"外层依赖内层";
- 跨边界的数据永远是简单的数据结构(DTO / 函数参数 / POJO),绝不直接传递业务实体对象或数据库行结构——那会偷偷引入指向外层的依赖。
Presenter 的工作也很值得注意:它把 OutputData 中的 Date、Currency 对象预先格式化成字符串,把按钮名称、是否置灰的开关都放进 ViewModel。于是 View 只剩下一件机械的事:把 ViewModel 搬进 HTML。
10. 展示器与谦卑对象
谦卑对象(Humble Object)模式是贯穿架构边界的通用模式:
难以测试的代码(GUI、数据库网关、外部服务客户端)应该被压缩成尽量薄的"谦卑对象",把可测试的逻辑抽到旁边的对象里。
- GUI:View 是谦卑对象(只搬数据,不测试);Presenter 是可测试对象(负责格式化、决定按钮置灰)。
- 数据库网关:SQL/ORM 部分是谦卑对象;用例与网关之间的接口实现是逻辑所在。
- 服务网关:类似处理。
11. 不完全边界:架构的成本控制
完整边界(双向接口 + 依赖反转)的构建和维护成本不低。敏捷社区会拿 YAGNI(“你不会需要它”)来反对预防性设计,但架构师的本职就是做预见。折中方案是不完全边界(Partial Boundary),书给了三种:
graph TD
subgraph 方案一["方案一:省掉最后一步"]
C1[Client] --> SB1[组件A 与组件B<br/>已按完整边界设计好接口与数据格式<br/>但合并为单个组件编译部署]
end
subgraph 方案二["方案二:单向边界(策略模式)"]
C2[Client] --> IF[ServiceBoundary 接口]
IMPL2[ServiceImpl] -.实现.-> IF
end
subgraph 方案三["方案三:门户模式 Facade(最简)"]
C3[Client] --> F[Facade 类<br/>只做调用转发]
F --> S1[服务函数1]
F --> S2[服务函数2]
end
三种方式成本递减、保护强度也递减。实践启示:哪怕预算有限,也要至少用单向接口把边界的位置"占住",给将来升级成完整边界留好路。
12. Main 组件:系统的"脏秘密"
每个系统都有一个最底层的细节:入口 Main 组件。它是:
- 初始细节的所在地:负责创建所有工厂、策略、全局设施;
- 最大的"具体"组件:它知道所有依赖关系,负责用依赖注入把它们组装起来;
- 系统中唯一允许"知道一切"的组件。
可以把它理解为插件系统的总装车间:所有插件都插到 Main 上,而插件们彼此互不相识。Spring 的 ApplicationContext 初始化代码,本质上就是 Main 组件。
13. 服务与测试
- 服务化(SOA/微服务)的收益被高估了:服务之间的解耦往往不彻底(共享数据、共享网络),服务边界并不自动等于架构边界。架构边界由"依赖方向"决定,而不是由"是否拆成服务"决定。
- 测试也是系统的一部分:测试代码应该是系统中最独立的组件——通过测试 API 调用业务逻辑,而不是通过 UI 或真实数据库。测试套件本身也要遵守整洁架构,否则会脆弱到"一改代码就挂一片",反过来吓得团队不敢重构。
第六部分 实现细节:震碎三观的四章
这一部分只反复强调一件事:
数据库是细节。Web 是细节。框架是细节。它们都是可以被替换、被推迟决策的 IO 设备。
数据库只是实现细节
关系数据库本质上只是一种"把数据组织在磁盘上方便存取"的技术,它对业务逻辑而言就是一个细节。业务模型关注的是数据结构和操作,不是表结构和 SQL。今天用 MySQL,明天换 MongoDB,核心业务逻辑应该无感。
Web 是实现细节
Web 只是一种 IO 设备。过去几十年 GUI 技术换了一茬又一茬(桌面 → Web → 移动 → …),Web 也终将过时。架构要做的是把系统核心设计成与设备无关,让"通过 Web 交付"成为一种可插拔的交付方式。
应用程序框架是实现细节
框架作者对你的人生没有承诺——框架升级时的破坏式变更、框架的局限性绑架,都是真实风险。正确姿势:
与框架保持"单向婚姻":你可以依赖框架,但不让框架侵入核心业务代码。
不要从框架基类派生业务对象,不要在核心业务层用框架的注解——把框架摁在最外层。
案例分析:视频销售网站
书里用一个"在线卖视频的网站"贯穿分析了完整的架构过程:
- 先识别用例和 actor(购买者、管理员、作者等);
- 按 actor 和变更原因划分组件边界(视图、用例、实体、数据库各自成组);
- 用依赖反转保证所有依赖指向高层策略;
- 最后得到一个可以按团队分工并行开发、按需单体或分组件部署的结构。
结论:架构设计的输入不是技术选型,而是用例、行为者、变更原因。
拾遗(Simon Brown 执笔):打包方式的争论
最后一章讨论了四种代码组织方式,相当接地气:
graph TD
A["① 按层打包<br/>controllers/ services/ repositories/<br/>(经典但业务意图不明显)"]
B["② 按功能打包<br/>orders/ customers/ payments/<br/>(业务意图清晰)"]
C["③ 端口与适配器<br/>domain 与 infrastructure 分离<br/>(六边形架构风格)"]
D["④ 按组件打包<br/>粗粒度组件 + 严格的访问控制<br/>(Bob 大叔推荐,配合编译器/访问修饰符强制隔离)"]
A -.演进.-> B -.演进.-> C -.演进.-> D
要点是:无论选哪种,关键是用编译器(访问修饰符、模块系统)来强制执行依赖规则,而不是靠"大家自觉"。架构约定如果没有工具强制,迟早被腐蚀。
实战举例:一个订单服务的"整洁化"改造
光看理论容易晕,用一个简化的 Java/Spring 风格例子收尾。
改造前(典型的三层贫血架构,业务逻辑泄漏到各处):
// Controller 直接调 Repository,业务规则散落在 SQL 和 Service 里
@RestController
class OrderController {
@Autowired OrderRepository repo; // 直接依赖 JPA/SQL
@PostMapping("/orders")
void create(@RequestBody OrderEntity e) {
if (e.getAmount() > 10000 && !e.getVip()) {
throw new RuntimeException("超限"); // 业务规则混在 Web 层
}
repo.save(e);
}
}问题:想换成命令行下单?重写。想换掉数据库?重写。想单测这条"超 1 万必须是 VIP"的规则?得把 Spring 容器和数据库拉起来。
改造后(按整洁架构分层):
// ① 实体层:与企业无关环境的核心规则,零依赖
class OrderPolicy {
static boolean canPlace(long amount, boolean vip) {
return amount <= 10000 || vip;
}
}
// ② 用例层:定义输入/输出端口,依赖反转
interface OrderGateway { // 用例层定义的接口(输出端口)
void save(String orderId, long amount, boolean vip);
}
class PlaceOrderUseCase {
private final OrderGateway gateway;
PlaceOrderUseCase(OrderGateway gateway) { this.gateway = gateway; }
String execute(long amount, boolean vip) {
if (!OrderPolicy.canPlace(amount, vip))
return "REJECTED";
String id = UUID.randomUUID().toString();
gateway.save(id, amount, vip);
return id;
}
}
// ③ 接口适配层 + 最外层:Controller / Repository 实现端口
@RestController
class OrderController { // Controller 是适配器
private final PlaceOrderUseCase useCase;
OrderController(PlaceOrderUseCase u) { this.useCase = u; }
@PostMapping("/orders")
ResponseEntity<String> create(@RequestBody OrderRequest req) {
return ResponseEntity.ok(useCase.execute(req.amount(), req.vip()));
}
}
@Repository
class SqlOrderGateway implements OrderGateway { // 数据库是插件
// 这里可以写 SQL/JPA,内层完全不知道
public void save(String id, long amount, boolean vip) { /* INSERT ... */ }
}改造带来的可验证收益:
- 可测试:
new PlaceOrderUseCase(inMemoryGateway)直接跑单元测试,不需要 Spring、不需要数据库; - 可替换 UI:再加一个
OrderCliRunner调用同一个PlaceOrderUseCase,一行用例代码不改; - 可替换数据库:写一个
MongoOrderGateway implements OrderGateway,在 Main(配置类)里换掉注入的实现即可。
这个例子里每一条收益,都直接来自"依赖关系规则"那一条铁律。
全书要点一页纸
| 部分 | 核心问题 | 一句话答案 |
|---|---|---|
| 概述 | 架构是为了什么 | 最小化系统全生命周期的人力成本 |
| 编程范式 | 代码的底层约束 | 三大范式都是"剥夺能力":限制 goto、函数指针、赋值 |
| SOLID | 类与函数怎么设计 | 中层五原则,其中 DIP 是架构边界的武器 |
| 组件原则 | 类怎么打包、组件怎么依赖 | 聚合(REP/CCP/CRP)与耦合(ADP/SDP/SAP)的权衡 |
| 软件架构 | 系统怎么分层 | 划边界 + 依赖规则:依赖永远指向高层策略 |
| 实现细节 | 数据库/Web/框架是什么 | 都是细节、都是插件、都可以推迟决策 |
如果只能记住一句话:
好的架构让最重要的东西(业务规则)不依赖任何东西,让最不重要的东西(数据库、Web、框架)依赖业务规则——依赖方向永远由外向内。
参考资料:《Clean Architecture: A Craftsman’s Guide to Software Structure and Design》,Robert C. Martin 著,中文译本。