关于 clean-architecture
介绍 Clean Architecture、Hexagonal Architecture、Onion Architecture 和 DDD 模式的架构参考资料。供开发者和团队用于隔离业务逻辑、划分系统边界,并保持领域规则可测试。
主要功能
- 01说明依赖向内的原则与分层职责
- 02介绍端口、适配器和架构边界
- 03讲解 DDD 战术与战略模式
- 04指导评估 CQRS 与事件溯源
- 05提供判断何时避免额外复杂度的依据
适用场景
- 01规划领域逻辑复杂应用的边界
- 02使业务规则脱离基础设施进行测试
- 03构建具有多个入口的系统
- 04评估 CQRS 是否适合读写需求
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ krzysztofsurdy/ code-virtuoso --skill clean-architecture安装后在 Claude Code 中输入/clean-architecture确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/context-mapping.mdddd-patterns.mdlayers.md详细介绍
整洁架构与 DDD
用于构建系统的架构模式和战术设计技术。这类系统将业务逻辑隔离出来,使其可测试,并且独立于框架、数据库和交付机制。其理论源自 Robert C. Martin、Alistair Cockburn 和 Eric Evans 的研究。
适用场景
- 领域包含值得明确建模的重要业务规则
- 系统必须能够适应框架升级、数据库迁移或交付机制变更
- 多个入口(API、CLI、消息使用者、定时任务)共用相同的业务逻辑
- 产品生命周期长,维护成本高于初期开发速度
- 团队规模或人员流动要求划分清晰边界并执行约定
- 可测试性是优先事项——业务规则必须能够在不依赖基础设施的情况下验证
不适用场景
并非所有系统都能从这种程度的架构严谨性中受益。遇到以下情况时,应避免过度设计:
- 应用只是简单的 CRUD 封装,没有重要的业务逻辑
- 项目是短期原型或概念验证
- 团队规模很小,且对领域已有充分了解,规则也很少
- 分层带来的开销超过了问题本身的复杂度
- 简单的以框架为中心的方案(例如带 Active Record 的 MVC)足以满足需求
关键问题是:领域是否足够复杂,值得投入这番功夫? 如果答案是否定的,那么选择更简单的架构才是正确做法。随着复杂度增加,你随时可以再引入边界。
架构风格
三种知名风格遵循相同的核心原则:依赖指向内侧,领域位于中心。
| 风格 | 起源 | 关键比喻 | 区别性理念 |
|---|---|---|---|
| 整洁架构 | Robert C. Martin(2012) | 同心圆 | 明确的层名称:实体、用例、接口适配器、框架 |
| 六边形架构(端口与适配器) | Alistair Cockburn(2005) | 带端口的六边形 | 驱动(主)适配器与被驱动(次)适配器之间的对称性 |
| 洋葱架构 | Jeffrey Palermo(2008) | 洋葱层 | 强调以领域模型为核心,基础设施位于最外层 |
它们的共同点:
- 领域逻辑位于中心,不依赖外部组件
- 依赖规则:源代码依赖始终指向内侧
- 基础设施和交付相关事项位于外层
- 通信通过抽象(接口/端口)跨越边界
在实际应用中,它们的区别只是命名约定。以下原则适用于这三种风格。
端口与适配器的概念
六边形架构引入了一套实用的术语,适用于这三种风格:
- 端口是由应用定义的接口。它描述应用需要外部世界提供什么(被驱动/次端口),或外部世界可以要求应用做什么(驱动/主端口)。
- 适配器是将端口连接到特定技术的实现。数据库适配器实现存储库端口。HTTP 控制器是驱动端口的适配器。
- 驱动(主)适配器发起与应用的交互:HTTP 控制器、CLI 命令、消息使用者、定时任务。
- 被驱动(次)适配器由应用调用:数据库存储库、电子邮件发送器、支付网关、文件存储。
这种对称性非常强大:应用无需知道驱动它的是 REST API 还是 CLI 命令,也无需知道它持久化到的是关系型数据库还是内存存储。
依赖规则
源代码依赖必须指向内层。内层的任何内容都不能了解外层的任何内容——包括名称、类型、接口或概念。
这是最重要的一条规则。其他所有准则都由此而来。
“指向内层”的含义:
- 内层定义接口(端口);外层负责实现
- 数据以简单结构(DTO、原始类型)的形式跨越边界,绝不使用框架对象
- 领域层绝不导入基础设施层、表示层或应用层的内容
- 应用层绝不导入基础设施层或表示层的内容
为什么这很重要:
- 无需数据库、HTTP 服务器或消息代理,也能测试领域层
- 框架可替换,只是实现细节
- 无需了解部署拓扑,也能读懂业务规则
如何落实:
- 使用静态分析或架构测试工具验证导入方向
- 将代码组织成体现各层的模块或包,让违规情况一目了然
- 代码审查应标记任何内层对外层类型的导入
- 组合根(应用程序入口点)是唯一负责连接所有层的地方——它是了解所有内容的例外
层参考
| 层 | 依赖方向 | 职责 | 应包含的内容 | 不应包含的内容 |
|---|---|---|---|---|
| 领域层(最内层) | 不依赖任何内容 | 业务规则、不变量 | 实体、值对象、聚合、领域服务、领域事件、仓储接口 | 框架导入、数据库查询、HTTP 概念 |
| 应用层 | 依赖领域层 | 用例编排 | 应用服务、命令/查询对象、DTO、端口接口 | 业务逻辑、直接调用基础设施 |
| 基础设施层 | 依赖应用层 + 领域层 | 技术能力 | 仓储实现、API 客户端、消息队列适配器、文件系统访问 | 业务规则、用例编排 |
| 表示层(最外层) | 依赖应用层 | 用户/系统交互 | 控制器、CLI 命令、API 端点、视图模型 | 业务逻辑、直接访问数据库 |
有关各层的深入指南,请参阅 层级详解。
DDD 战术模式
战术模式为领域层提供结构。每种模式在架构中都有特定职责和位置。
| 模式 | 目的 | 层 | 核心规则 |
|---|---|---|---|
| 实体 | 具有身份和生命周期的对象 | 领域层 | 身份决定相等性 |
| 值对象 | 不可变、无身份的概念 | 领域层 | 按属性判断相等,不产生副作用 |
| 聚合 | 一致性边界 | 领域层 | 每个事务处理一个聚合,其他聚合通过 ID 引用 |
| 仓储 | 以类似集合的方式访问聚合 | 领域层中的接口,基础设施层中的实现 | 每个聚合根对应一个仓储 |
| 领域服务 | 跨实体的无状态逻辑 | 领域层 | 仅当逻辑不属于单个实体时使用 |
| 领域事件 | 对已发生事项的记录 | 领域层 | 以过去时命名,载荷不可变 |
| 应用服务 | 用例编排者 | 应用层 | 不包含业务逻辑——委托给领域对象处理 |
| 工厂 | 创建复杂对象 | 领域层 | 封装构造不变量 |
有关各模式的详细指南,请参阅 DDD 战术模式。
DDD 战略模式
战略模式关注系统的大规模结构——如何将复杂领域划分为易于管理的部分,以及这些部分如何交互。
有界上下文
有界上下文是定义并保持特定领域模型一致性的边界。每个上下文都有自己的通用语言——同一个词在不同上下文中可能有不同的含义。
识别边界:
- 不同团队或部门通常意味着不同的上下文
- 当同一个术语(例如“Account”)对不同利益相关者有不同含义时,它们应属于不同的上下文
- 上下文应足够小,便于一个团队负责
- 根据业务能力划分上下文,而不是根据技术层
通用语言
在每个有界上下文中,团队(开发人员和领域专家)都使用精确的词汇。代码应直接使用这种语言——类名、方法名和变量名应准确反映领域概念,符合领域专家的描述方式。
维护通用语言的实用建议:
- 如果开发人员无法向领域专家解释某个类名,那么这个名字就不恰当
- 语言演变时要重构代码——重命名不只是表面工作,而是一项设计活动
- 避免在领域层代码中使用技术术语——使用业务术语,而不是实现术语
- 将这套语言记录在开发人员和领域专家共享的术语表中
上下文映射
上下文映射记录有界上下文之间的关系。它明确集成策略,避免集成方式由偶然因素决定。
| 模式 | 关系 | 适用场景 |
|---|---|---|
| 共享内核 | 两个上下文共享模型的一个子集 | 团队紧密协作,并愿意协调变更 |
| 客户-供应商 | 上游上下文服务于下游上下文 | 下游能够与上游协商 |
| 遵从者 | 下游遵从上游模型 | 没有协商能力;上游不会改变 |
| 防腐层 | 转换层保护下游 | 与遗留系统或外部系统集成 |
| 开放主机服务 | 上游提供定义清晰的协议 | 多个使用方需要稳定的访问方式 |
| 发布语言 | 共享的交换格式 | 通过标准模式进行跨上下文通信 |
有关集成模式和边界决策,请参阅上下文映射。
防腐层
与外部系统或遗留系统集成时,防腐层(ACL)会在外部模型与内部领域模型之间进行转换,防止外部概念渗入领域模型。
结构: 外部系统 -> ACL(适配器 + 转换器)-> 领域模型
ACL 属于基础设施层,并实现由应用层或领域层定义的端口。
CQRS — 命令查询职责分离
CQRS 是整洁架构和 DDD 的天然搭档。它将写模型(命令)与读模型(查询)分离,使两者能够分别优化。
它如何融入架构:
- 命令经由应用层流转,修改聚合,并通过仓储持久化
- 查询可以完全绕过领域层,读取经过优化的投影或视图
- 写侧通过领域模型强制执行业务规则
- 读侧针对使用方的需求进行优化——用于展示时,无需重建完整聚合
适合考虑使用 CQRS 的情况:
- 读写模式差异显著(例如写入复杂但读取简单,或反之)
- 读写的性能要求不同
- 读模型需要无法映射到领域模型的反规范化视图
- 正在使用事件溯源(CQRS 几乎总是与事件溯源配套使用)
不适合使用 CQRS 的情况:
- 读写对称的简单 CRUD 应用
- 增加的复杂度与领域需求不相称
事件溯源 — 简要概述
事件溯源将聚合的状态存储为一系列领域事件,而非当前状态快照。当前状态通过重放事件得出。
与整洁架构的关系:
- 事件属于领域层
- 事件存储属于基础设施关注点(专用仓储)
- 投影(读取模型)基于事件,在基础设施层或应用层构建
- 领域模型不依赖事件存储的实现
适用场景: 需要审计、状态转换复杂、需要进行时间查询(“时间 X 时的状态是什么”),或者领域事件已在设计中居于核心地位。
常见误解
- “整洁架构意味着大量样板代码” — 分层会增加一些结构性代码,但换来的是明确的边界。如果样板代码多到难以承受,说明领域可能还不够复杂,不值得采用这种架构。
- “每个应用都需要全部四层” — 小型应用可以合并应用层和表示层。关键边界在领域层与基础设施层之间。
- “到处使用 DTO 就是整洁架构” — DTO 是跨越边界的一种机制,并非架构本身。架构关注的是依赖方向。
- “领域模型必须映射数据库架构” — 实际情况恰恰相反。领域模型反映业务概念,基础设施层负责在领域模型与存储架构之间进行映射。
- “六边形架构和整洁架构是两回事” — 它们是对同一核心理念的不同描述。选择团队更认同的说法即可。
- “DDD 要求使用整洁架构” — DDD 战术模式可以在不严格分层的情况下使用,不过二者搭配效果最佳。反过来,整洁架构也不要求使用 DDD 模式。
- “一个限界上下文 = 一个微服务” — 限界上下文是逻辑边界。它可以对应一个微服务、单体应用中的一个模块,或任何其他部署单元。
常见违规情况
| 违规情况 | 症状 | 修复方法 |
|---|---|---|
| 领域层依赖框架 | 领域类导入 HTTP、ORM 或框架注解 | 将基础设施关注点移至外层;在领域层使用普通对象 |
| 控制器中包含业务逻辑 | 控制器包含条件逻辑、验证规则或计算 | 将逻辑提取到领域对象或应用服务中 |
| 贫血领域模型 | 实体只是包含数据的容器,只有 getter/setter;所有逻辑都位于服务中 | 将行为移入实体和值对象;在聚合内部强制执行不变量 |
| 应用服务包含业务规则 | 应用层包含复杂条件逻辑和领域计算 | 将规则移至领域服务或实体方法中 |
| 基础设施关注点渗入内层 | 领域层引用数据库列名或 API 响应结构 | 在基础设施层引入映射;保持领域模型纯粹 |
| 上帝聚合 | 一个聚合处理过多关注点,不断膨胀 | 拆分为更小的聚合;使用领域事件进行跨聚合通信 |
| 不同上下文共用数据库 | 多个限界上下文读写相同的表 | 为每个上下文分离存储;通过事件或 API 集成 |
| 层之间存在循环依赖 | 内层导入外层,导致编译错误或运行时耦合 | 重新审视接口应由哪一层负责;反转依赖方向 |
| 跨边界复用 DTO | API 响应、用例输入和领域逻辑使用相同的数据结构 | 为每个边界创建独立的 DTO;在它们之间进行映射 |
| 缺少防腐层 | 外部系统的模型污染领域词汇 | 在基础设施层添加转换层 |
测试策略
分层架构有助于制定清晰的测试策略,每一层都有独特的测试方法:
| 层 | 测试类型 | 依赖项 | 验证内容 |
|---|---|---|---|
| 领域层 | 单元测试 | 无 — 纯逻辑 | 业务规则、不变量、值对象相等性、聚合一致性 |
| 应用层 | 使用模拟端口的单元测试 | 领域层 + 模拟的基础设施端口 | 用例编排、是否正确调用领域方法、事务边界 |
| 基础设施层 | 集成测试 | 真实外部系统(数据库、API、队列) | 映射是否正确、查询行为、适配器保真度 |
| 表现层 | 功能测试 / 验收测试 | 完整技术栈或模拟的应用层 | 请求解析、响应格式化、错误处理、状态码 |
核心原则: 大多数测试应当是快速的领域单元测试。基础设施集成测试数量较少,但可确保真实系统按预期运行。端到端测试覆盖关键路径,但应尽量精简。
质量检查清单
使用此检查清单评估架构的结构是否合理:
依赖规则
- 领域层不导入应用层、基础设施层或表现层的任何内容
- 应用层不导入基础设施层或表现层的任何内容
- 所有跨边界通信均使用内层定义的接口/端口
领域层
- 实体维护自身的不变量 — 无效状态无法表示
- 值对象不可变
- 聚合是保持一致性的单位 — 每个事务处理一个聚合
- 仓储接口定义在领域层,而非基础设施层
- 领域事件记录有意义的状态变化
应用层
- 应用服务负责编排,但不包含业务逻辑
- 用例定义明确 — 每个用例都有清晰的输入、输出和单一职责
- 使用 DTO 跨边界传递数据 — 领域对象不会泄漏到外层
基础设施层
- 仓储实现位于此层,而非领域层
- 外部服务集成使用实现端口的适配器
- 框架特定代码彼此隔离且可替换
表现层
- 控制器保持精简 — 立即将任务委托给应用服务
- 控制器、视图或 CLI 命令中不包含业务逻辑
- 输入校验(格式、类型)在此处进行;业务校验在领域层进行
限界上下文
- 每个上下文都有自己的通用语言
- 上下文通过定义明确的接口(事件、API)通信,而非共享数据库
- 防腐层可防止外部模型污染
测试
- 无需任何基础设施即可测试领域逻辑
- 使用模拟端口即可测试应用服务
- 基础设施适配器有针对真实依赖项的集成测试
- 验证业务规则的测试无需启动框架
最佳实践
- 从领域入手 — 先对业务规则建模,再添加基础设施
- 根据领域概念命名,而非技术模式(避免使用 SomethingManager、SomethingHelper)
- 保持聚合小巧 — 大型聚合是设计异味,表明边界划分不当
- 在所有层中优先使用组合,而非继承
- 使用领域事件在聚合之间通信,而非直接耦合
- 让无效状态无法表示 — 将校验逻辑放入值对象和实体构造函数
- 保持应用层精简 — 如果其中包含业务逻辑,说明领域层过于贫血
- 通过重构不断加深理解 — 第一个领域模型很少是最佳模型;随着理解加深,持续演进模型
- 使用架构测试自动强制执行依赖规则
- 逐步引入整洁架构 — 从最复杂的子域开始,再逐步扩展