
clean-architecture
热门围绕依赖原则组织软件:源代码依赖从框架指向用例,再指向实体,方向向内。当用户提到“架构分层”、“依赖原则”、“端口与适配器(六边形架构)”、“洋葱架构”、“尖叫架构”、“业务逻辑应该放在哪里”、“与数据库解耦”、“无需重写即可替换框架”或“保持业务规则独立”时使用。也适用于决定代码属于哪一层、将核心逻辑与基础设施隔离、定义模块边界,或讨论框架是否应该调用你的代码还是反过来。涵盖组件原则、边界和SOLID。关于代码级质量,请参考clean-code。关于领域建模,请参考domain-driven-design。
围绕依赖原则组织软件:源代码依赖从框架指向用例,再指向实体,方向向内。当用户提到“架构分层”、“依赖原则”、“端口与适配器(六边形架构)”、“洋葱架构”、“尖叫架构”、“业务逻辑应该放在哪里”、“与数据库解耦”、“无需重写即可替换框架”或“保持业务规则独立”时使用。也适用于决定代码属于哪一层、将核心逻辑与基础设施隔离、定义模块边界,或讨论框架是否应该调用你的代码还是反过来。涵盖组件原则、边界和SOLID。关于代码级质量,请参考clean-code。关于领域建模,请参考domain-driven-design。
整洁架构框架
一种严谨的软件结构方法,使业务规则独立于框架、数据库和交付机制。在设计系统架构、审查模块边界或提供依赖管理建议时应用这些原则。
核心原则
源代码依赖必须指向内——朝向更高层次的策略。 内圈中的任何内容都不能知道外圈的任何内容。这一单一规则产生了可测试且独立于框架、UI、数据库和任何外部机构的系统。业务规则才是重要的;数据库、Web框架和交付机制都是细节——当细节依赖于策略时,你可以推迟决策、替换实现,并隔离测试业务逻辑。
评分
目标:10/10。 快速诊断的七行中每满足一行得1分(0-7),然后映射到0-10区间:6-7行满足 = 9-10(依赖原则成立,业务逻辑独立于框架和数据库);4-5 = 6-8(核心可测试但某些细节向内泄露);2-3 = 3-5(框架或持久化决定了结构);0-1 = 0-2(无边界——业务规则存在于控制器和ORM模型中)。报告分数、未满足的诊断行以及修复每个问题所需的具体反转。
1. 依赖原则与同心圆
核心概念: 将架构组织为同心圆——最内层是实体(企业业务规则),然后是用例(应用业务规则),接着是接口适配器,最外层是框架和驱动程序。源代码依赖始终指向内。
为什么有效: 当高层策略不依赖于低层细节时,你可以替换数据库、Web框架或API风格而不影响业务逻辑——系统对栈中最易变的部分具有弹性。
关键见解:
- 内圈不能提及外圈名称——不能有来自外部的类、函数、变量或数据格式
- 跨越边界的数据必须以对内圈最方便的形式传递,绝不由外圈决定
- 依赖反转(接口在内圈定义,在外圈实现)是强制该规则的机制
- 圆圈数量不固定——典型的是四个;规则保持不变
- 框架是细节,不是架构——它们属于最外层
代码应用:
| 上下文 | 模式 | 示例 |
|---|---|---|
| 层方向 | 内圈定义接口;外圈实现 | UserRepository 接口在用例层;PostgresUserRepository 在适配器层 |
| 数据跨越 | DTO跨越边界,而非ORM实体 | 用例返回 UserResponse DTO,而不是ActiveRecord模型 |
| 依赖方向 | 导入箭头始终指向内 | 控制器导入用例;用例从不导入控制器 |
当内圈导入指向外圈时,请参阅 references/dependency-rule.md 以获取四圈代码演练、数据跨越规则以及修复问题的四步依赖反转过程。
2. 实体与用例
核心概念: 实体封装企业范围的业务规则——即使没有软件也会存在的规则。用例包含应用特定的规则,协调数据流入和流出实体。
为什么有效: 将业务做什么(实体)与应用如何协调它(用例)分离,使你能够在多个应用中重用实体,并在不改变核心业务规则的情况下更改应用行为。
关键见解:
- 实体不是数据库行——它们是封装关键业务规则的对象或纯函数
- 用例接受请求模型并返回响应模型——绝不使用框架对象
- 每个用例是一个单一的应用操作(
CreateOrder、ApproveExpense) - 交互器模式:用例类实现输入边界接口并调用输出边界接口
- 对用例的更改不应影响实体;实体的更改可能波及用例
代码应用:
| 上下文 | 模式 | 示例 |
|---|---|---|
| 实体设计 | 关键业务规则,零框架依赖 | Order.calculateTotal() 应用税务规则;对HTTP一无所知 |
| 请求/响应 | 简单数据结构跨越边界 | CreateOrderRequest { items, customerId } —— 无ORM模型 |
| 单一职责 | 每个操作一个用例 | PlaceOrder、CancelOrder、RefundOrder 作为单独的类 |
| 交互器 | 实现输入端口,调用输出端口 | PlaceOrderInteractor implements PlaceOrderInput |
在设计交互器或决定什么属于实体与用例时,请参阅 references/entities-use-cases.md —— 包含企业与应用业务规则的完整处理以及请求/响应模型示例。
3. 接口适配器与框架
核心概念: 接口适配器在用例/实体方便的形式与外部机构要求的形式之间转换数据。框架和驱动程序是最外层——与外部世界的粘合代码。
为什么有效: 当Web框架、ORM或消息队列被限制在外圈时,替换其中任何一个都是局部更改。数据库是细节;Web是细节;细节应该是业务规则的插件,而不是应用的骨架。
关键见解:
- 控制器将HTTP转换为用例输入;展示器将用例输出转换为视图模型
- 网关实现由用例定义的仓库接口——内圈定义契约,外圈履行契约
- 业务规则永远不知道数据存储在SQL、NoSQL还是平面文件中,也不知道交付是HTTP
- 对框架保持警惕——它们希望你耦合到它们;保持距离
代码应用:
| 上下文 | 模式 | 示例 |
|---|---|---|
| 控制器 | 交付机制 → 用例输入 | OrderController.create(req) 构建 CreateOrderRequest,调用交互器 |
| 展示器 | 用例输出 → 视图模型 | OrderPresenter.present(response) 格式化为JSON/HTML |
| 网关 | 仓库接口按数据库实现 | SqlOrderRepository implements OrderRepository |
| 框架边界 | 框架向内调用,绝不反向 | Express路由处理器调用控制器;控制器从不导入Express |
在连接控制器、展示器或网关,或争论数据库/Web是细节时,请参阅 references/adapters-frameworks.md —— 涵盖插件架构以及如何将框架限制在边缘。
4. 组件原则
核心概念: 组件是部署单元。三个内聚原则决定组件内部包含什么;三个耦合原则决定组件之间的关系。
为什么有效: 组合不当的组件会产生连锁效应,一个更改迫使不相关的代码重新部署;这些原则使更改局部化,发布独立。
关键见解:
- REP(复用/发布等价):组件中的类必须可版本化并作为一个单元发布
- CCP(共同闭包):因相同原因同时更改的类应放在一起——组件的SRP
- CRP(共同复用):不要强制用户依赖他们不使用的类
- ADP(无环依赖):组件图必须无环——通过DIP或新组件打破循环
- SDP(稳定依赖):依赖方向应朝向稳定性
- SAP(稳定抽象):稳定的组件应该是抽象的;不稳定的组件应该是具体的
代码应用:
| 上下文 | 模式 | 示例 |
|---|---|---|
| 组件分组 | 将一起更改的类分组(CCP) | 所有与订单相关的用例放在一个组件中 |
| 打破循环 | 应用DIP反转依赖边 | 将接口提取到新组件中以打破循环 |
| 稳定性度量 | 不稳定性 I = Ce / (Ca + Ce) | 入向依赖多,出向依赖少 → I接近0(稳定) |
在将类分组为可部署组件或打破依赖循环时,请参阅 references/component-principles.md —— 每个原则(REP、CCP、CRP、ADP、SDP、SAP)都通过不稳定性度量进行了详细说明。
5. SOLID原则
核心概念: 五个类与模块级原则——单一职责、开闭、里氏替换、接口隔离、依赖反转——使依赖原则成为可能的中级构建块。
为什么有效: 每个原则针对依赖出错的特定方式,防止导致代码库变成遗留噩梦的僵化、脆弱和不可移动性。
关键见解:
- SRP:一个模块只有一个更改理由——它服务于一个角色(不是“做一件事”)
- OCP:通过添加新代码扩展行为,而不是修改现有代码——策略和插件模式
- LSP:子类型必须能够通过基接口使用,而客户端无需知道——被意外异常或忽略的方法违反
- ISP:客户端不应依赖它们不使用的方法——胖接口造成不必要的耦合
- DIP:高层模块和低层模块都依赖于由高层模块定义的抽象
代码应用:
| 上下文 | 模式 | 示例 |
|---|---|---|
| SRP违反 | 类服务于多个角色 | Employee 处理薪资(CFO)、报告(COO)、持久化(CTO) |
| OCP通过策略 | 通过新类添加新行为 | 添加 ExpressShipping 实现 ShippingStrategy;Order 不变 |
| LSP违反 | 子类型改变预期行为 | Square extends Rectangle 破坏了 setWidth()/setHeight() 契约 |
| ISP应用 | 将胖接口拆分为角色接口 | Printer、Scanner、Fax 而不是一个 MultiFunctionDevice |
| DIP连接 | 高层定义接口;低层实现 | OrderService 依赖于 PaymentGateway,而不是 StripeClient |
在将SRP/OCP/LSP/ISP/DIP应用于特定类或诊断违反时,请参阅 references/solid-principles.md —— 每个原则都通过代码示例及其防止的坏味道进行了详细说明。
6. 边界与边界剖析
核心概念: 边界是重要事物与细节之间的界线,通过多态实现:依赖关系指向内,而控制流可以双向跨越。
为什么有效: 每个边界都为你提供了推迟决策或替换实现的选择;战略性的边界放置决定了系统在多年维护中是令人愉悦还是痛苦。
关键见解:
- 完整边界在两侧使用相互接口;部分边界使用更简单的策略或外观
- 谦卑对象模式:将边界代码拆分为难以测试的部分(靠近边界)和易于测试的部分(逻辑)
- 服务不自动成为架构边界——具有庞大共享数据模型的微服务是带有网络调用的单体
- 测试是最孤立的组件:它们向内依赖,没有东西依赖它们
- 过早的边界代价高昂,但缺失的边界同样代价高昂——在可能变化的点绘制边界
代码应用:
| 上下文 | 模式 | 示例 |
|---|---|---|
| 完整与部分边界 | 相互端口,或单独的策略 | 用例定义 PlaceOrderInput/PlaceOrderOutput;简单情况采用 ShippingStrategy |
| 谦卑对象 | 将可测试逻辑与基础设施分离 | PresenterLogic(可测试)生成 ViewModel;View(谦卑)渲染它 |
| Main作为插件 | 组合根组装系统 | main() 连接所有具体实现并启动应用 |
在决定在哪里绘制边界、选择完整与部分边界或应用谦卑对象模式时,请参阅 references/boundaries.md —— 还涵盖服务作为边界、测试边界以及Main作为终极插件。
常见错误
| 错误 | 为什么失败 | 修复 |
|---|---|---|
| ORM泄露到业务逻辑 | 实体耦合到模式;数据库更改重写业务规则 | 将领域实体与持久化模型分离;在适配器层映射 |
| 业务规则在控制器中 | 没有HTTP无法测试;跨端点重复 | 将逻辑移入用例交互器;控制器仅转换和委托 |
| 框架优先架构 | 框架决定结构;替换意味着重写 | 将框架视为插件;按业务能力组织代码 |
| 循环组件依赖 | 更改不可预测地传播;无法独立发布 | 应用DIP或提取共享抽象组件 |
| 每个功能一个巨型用例 | 臃肿的千行协调器 | 拆分为专注的单操作用例 |
| “因为简单”而跳过边界 | 耦合悄然累积,直到代价巨大 | 在可能变化的点主动绘制边界 |
| 微服务自动成为好架构 | 分布式单体比干净的单体更差 | 在服务内部和跨服务应用依赖原则;服务是部署边界,不是架构边界 |
快速诊断
| 问题 | 如果否 | 操作 |
|---|---|---|
| 能否在没有数据库、Web服务器或框架的情况下测试业务规则? | 规则耦合到基础设施 | 将实体和用例提取到接口后面;模拟外层 |
| 所有源代码依赖是否指向内? | 违反依赖原则 | 引入边界接口;反转违规依赖 |
| 能否在不触及业务逻辑的情况下替换数据库? | 持久化向内泄露 | 仓库模式;将持久化隔离在适配器中 |
| 用例是否独立于交付机制? | 用例知道HTTP/CLI/队列 | 在用例签名中使用普通DTO |
| 框架是否被限制在最外层? | 框架就是你的架构 | 将框架调用包装在接口后面;推到边缘 |
| 组件图是否无环? | 存在循环依赖 | 应用ADP:通过DIP或新组件打破每个循环 |
| Main(组合根)是否连接所有依赖? | 具体类在内圈实例化 | 将构造移到Main;使用DI或工厂 |
延伸阅读
基于Robert C. Martin关于软件架构的权威指南:
- 《整洁架构:软件结构与设计的工匠指南》 by Robert C. Martin
关于作者
Robert C. Martin(“鲍勃大叔”) 自1970年起从事软件工程,是敏捷宣言的创始签署人之一,也是《整洁代码》、《代码整洁之道》、《整洁架构》和《整洁敏捷》的作者。他的SOLID原则是面向对象设计的基础词汇,他的工作主张架构是关于管理依赖关系并使业务规则独立于基础设施细节。





