📚 本文基于《Clean Architecture:整洁架构之道》[Robert C. Martin (Uncle Bob), 电子工业出版社] 系统整理,同时融合了实际项目中的实践经验与踩坑记录。
Robert C. Martin(Uncle Bob)是软件工程界的传奇人物,敏捷联盟(Agile Alliance)的创始人之一,SOLID 原则的提出者。他在软件开发方法论、设计原则和架构思想方面有着超过50年的从业经验。他的著作还包括《代码整洁之道》(Clean Code)和《程序员的职业素养》(The Clean Coder),构成了完整的"整洁系列"。
《Clean Architecture》是 Uncle Bob 在架构层面的集大成之作。如果说《Clean Code》教我们如何写好函数和类,那么《Clean Architecture》教我们如何组织整个系统。本书不是一本具体技术的实操手册,而是一本架构哲学指南——它回答的是"好的架构应该长什么样"这个根本问题。
Uncle Bob 的核心论点是:好的架构应该让系统的业务逻辑独立于交付机制、框架、数据库和任何外部工具。他提出了依赖规则(Dependency Rule)作为架构的"宪法",所有架构决策都必须在这个规则框架内做。
架构的目标不是让系统工作——让系统工作是最低要求。架构的目标是降低系统的维护成本,让系统的变更成本与变更范围成正比,而不是与系统规模成正比。
传统分层架构(Presentation Layer → Business Layer → Data Access Layer)在实践中常常走向一个方向:分层被打破。典型的症状包括:
Uncle Bob 指出,软件系统的成本曲线并非线性。如果架构糟糕,随着系统规模增长,变更成本会呈现指数级上升。这被称作"软件熵增"——如果不主动对抗,系统的混乱度必然增加。
一个反直觉的事实是:好的架构在项目初期看起来更"慢",因为你需要花时间搭建正确的边界和抽象层。但从中长期看,这种"慢"恰恰是更快的——它让系统在持续迭代中保持可预测的变更成本。
Clean Architecture 不是让系统变得更"复杂",而是让系统的复杂性被正确隔离。其核心追求:
依赖规则(Dependency Rule)是 Clean Architecture 的基石。它的表述非常简单:
源代码依赖只能指向内层,不能指向外层。
外圈 ┌──────────────────────┐
│ Frameworks & │
│ Drivers │
│ ┌────────────────┐ │
│ │ Interface │ │
│ │ Adapters │ │
│ │ ┌──────────┐ │ │
│ │ │ Use │ │ │
│ │ │ Cases │ │ │
│ │ │ ┌────┐ │ │ │
│ │ │ │ │ │ │ │
│ │ │ │Ent │ │ │ │
│ │ │ │ity │ │ │ │
│ │ │ └────┘ │ │ │
│ │ └──────────┘ │ │
│ └────────────────┘ │
└──────────────────────┘
这条规则意味着:
场景一:ORM 实体继承
// ❌ 错误:Entity 层引用了 ORM 框架
@Entity
public class User {
@Id
@GeneratedValue
private Long id;
}
正确的做法:Entity 是纯净的 POJO,ORM 映射通过外层适配器完成。
场景二:使用框架注解渗透业务逻辑
// ❌ 错误:Use Case 层依赖了框架注解
@Transactional
public class CreateOrderUseCase {
@Autowired
private OrderRepository repository;
}
正确的做法:Use Case 使用接口(端口)定义依赖,由外层适配器提供实现。
遵循依赖规则带来的直接收益:
定义:实体是企业级别的业务规则抽象。它们可能是整个系统的核心业务对象,也可能是操作这些对象的方法集合。
特征:
示例 - 订单实体:
public class Order {
private String id;
private List<OrderItem> items;
private OrderStatus status;
public Order(String id, List<OrderItem> items) {
this.id = id;
this.items = new ArrayList<>(items);
this.status = OrderStatus.PENDING;
}
public void complete() {
if (status != OrderStatus.PENDING) {
throw new IllegalStateException("Only pending orders can be completed");
}
this.status = OrderStatus.COMPLETED;
}
// getters...
// 实体中只包含业务判断,不包含依赖
}
定义:用例是特定应用场景下的业务操作流程。每个用例描述"用户如何与系统交互来完成一个业务目标"。
特征:
示例 - 创单用例:
public class CreateOrderUseCase {
private final OrderRepository repository;
private final PaymentService paymentService;
// 构造函数注入端口,端口由外层适配器实现
public CreateOrderUseCase(OrderRepository repository,
PaymentService paymentService) {
this.repository = repository;
this.paymentService = paymentService;
}
public CreateOrderResponse execute(CreateOrderRequest request) {
// 1. 校验用户
User user = repository.findUser(request.userId());
if (user == null) throw new UserNotFoundException();
// 2. 创建订单实体
Order order = new Order(generateId(), request.items());
// 3. 保存订单
repository.save(order);
// 4. 触发支付
paymentService.charge(order.totalAmount());
return new CreateOrderResponse(order.id(), OrderStatus.PENDING);
}
}
// 定义在用例层的接口(端口)
public interface OrderRepository {
void save(Order order);
User findUser(String userId);
}
public interface PaymentService {
void charge(Money amount);
}
注意:这里的 OrderRepository 和 PaymentService 是端口(Port),它们的实现(Adapter)在外层。用例层只定义这些接口,不关心具体实现。
定义:这一层负责将内层的数据格式转换为外层可以消费的格式,反之亦然。
职责:
示例 - Controller 与 Presenter:
// Controller:将 HTTP 输入转换为 Use Case 输入
@RestController
public class OrderController {
private final CreateOrderUseCase createOrderUseCase;
public OrderController(CreateOrderUseCase createOrderUseCase) {
this.createOrderUseCase = createOrderUseCase;
}
@PostMapping("/orders")
public ResponseEntity<CreateOrderResponse> createOrder(
@RequestBody CreateOrderRequestDTO requestDTO) {
// 将 HTTP 请求转换为 Use Case 输入
CreateOrderUseCase.CreateOrderRequest request =
new CreateOrderUseCase.CreateOrderRequest(
requestDTO.getUserId(),
requestDTO.getItems()
);
// 调用 Use Case
CreateOrderUseCase.CreateOrderResponse response =
createOrderUseCase.execute(request);
return ResponseEntity.ok(response);
}
}
// Repository 实现:数据库适配器
@Repository
public class JpaOrderRepository implements OrderRepository {
private final JpaOrderDao dao;
public JpaOrderRepository(JpaOrderDao dao) {
this.dao = dao;
}
@Override
public void save(Order order) {
// 实体 → 数据模型 的转换在这里完成
OrderEntity entity = new OrderEntity();
entity.setId(order.id());
entity.setStatus(order.status().name());
entity.setItems(convertItems(order.items()));
dao.save(entity);
}
}
定义:最外层,包含所有具体的实现细节。
组成:
关键原则:
SOLID 原则并非 Uncle Bob 为 Clean Architecture 单独创造的,但它们天然是 Clean Architecture 的支撑。下面逐一解读在 Clean Architecture 语境下的实践要点。
核心:一个模块应该有且只有一个引起它变更的原因。
在 Clean Architecture 中,SRP 体现在每一层的划分上:
实战技巧:不要因为"都是关于用户的操作"就把所有用户相关的逻辑放在一个类里。用户注册、用户登录、用户信息修改——这是三个不同的原因,应该分成三个 Use Case 类。
核心:对扩展开放,对修改关闭。
在 Clean Architecture 中,OCP 的典型应用是通过接口隔离:
// 开闭的体现:新增搜索功能不需要修改现有用例
public interface SearchPort {
List<Order> search(SearchCriteria criteria);
}
// 现有的实现
public class ElasticSearchAdapter implements SearchPort { ... }
// 新增的实现
public class MeiliSearchAdapter implements SearchPort { ... }
// Use Case 不需要任何修改
核心:子类型必须能够替换其基类型。
在 Clean Architecture 中,LSP 主要约束接口/抽象类的设计:
典型案例—违反 LSP:
// 接口:方形
interface Rectangle {
void setWidth(int w);
void setHeight(int h);
int getArea();
}
// ❌ 正方形扩展可能导致违反 LSP
class Square implements Rectangle {
void setWidth(int w) { super.setWidth(w); super.setHeight(w); }
void setHeight(int h) { super.setWidth(h); super.setHeight(h); }
}
// 使用方如果分别调用 setWidth 和 setHeight,结果不符合预期
核心:不应强迫客户端依赖它们不使用的接口。
在 Clean Architecture 中的实践:
// ❌ 错误:胖接口
interface UserService {
void register(RegisterRequest req);
UserDTO login(LoginRequest req);
void updateProfile(UpdateProfileRequest req);
void changePassword(ChangePasswordRequest req);
void deleteAccount(DeleteAccountRequest req);
}
// ✅ 正确:每个用例有自己的接口
interface RegisterUseCase {
void execute(RegisterRequest req);
}
interface LoginUseCase {
UserDTO execute(LoginRequest req);
}
核心:
这是 Clean Architecture 依赖规则的直接理论基础。整个架构的依赖方向正是通过 DIP 实现的。
形式:
Use Case (高层) → OrderRepository (接口/抽象)
↑
JpaOrderRepository (低层) ——— 实现
除了 SOLID 原则,Uncle Bob 还提出了三个组件构建原则,它们是 Clean Architecture 大粒度模块划分的指南。
核心:软件复用的粒度与发布的粒度是一致的。
意思是:一个组件如果不是为了被复用而设计的,就不应该被单独发布。反之,如果一个组件被单独发布,就应该设计为可复用的。
核心:将因相同原因而变更的类聚集到同一个组件中。
CCP 是 SRP 在组件级别的映射。在 Clean Architecture 中,它指导我们:
核心:不要强迫一个组件的使用者依赖它们不需要的东西。
CRP 是 ISP 的组件级别映射。它指导不要让组件变得"过于全面",而是保持专注,确保其他组件不会因为引入了不需要的依赖而被迫变更。
REP 和 CCP 倾向于让组件变大(便于复用和管理变更),CRP 倾向于让组件变小(防止不必要的依赖)。在实际项目中,需要在三者之间找到平衡点:
依赖注入是实现 Clean Architecture 中最常用的技术手段。在运行时,由外层(通常是 IoC 容器)将具体实现注入到内层。
典型结构:
// 最内层:实体
public class Order { ... }
// 第二层:用例层定义端口
public interface OrderRepository { ... }
// 第三层:适配器实现端口
public class JpaOrderRepository implements OrderRepository { ... }
// 第四层:框架层完成注入(Spring Boot 自动注入或手动配置)
@Configuration
public class ApplicationConfig {
@Bean
public CreateOrderUseCase createOrderUseCase(OrderRepository repo) {
return new CreateOrderUseCase(repo);
}
}
方向:外层 → 内层(通过内层定义的接口进行通信)
// 用例层定义端口
public interface EmailSender {
void send(Email email);
}
// 框架层实现端口
@Component
public class SmtpEmailSender implements EmailSender {
// 发送邮件...
}
方向:任何方向都可以,但传递的是简单数据结构(DTO)
// 所有层共享的简单数据模型
public record CreateOrderRequest(
String userId,
List<OrderItem> items
) {}
这种方式的优势:
方向:内层发布事件,外层订阅
// 用例层在业务完成后发布事件
public class CreateOrderUseCase {
private final EventPublisher publisher;
public OrderCreatedEvent execute(CreateOrderRequest request) {
// 业务逻辑...
Order order = createOrder(request);
// 发布事件
return new OrderCreatedEvent(order.id(), order.totalAmount());
}
}
// 外层适配器订阅事件
@Component
public class OrderEventHandler {
@EventListener
public void onOrderCreated(OrderCreatedEvent event) {
// 发送通知、记录审计日志等
}
}
| 维度 | 分层架构 | Clean Architecture |
|---|---|---|
| 依赖方向 | 从上到下 | 从外到内 |
| 边界清晰度 | 易被打破 | 强制遵守 |
| 数据库耦合 | 通常直接依赖 ORM | 通过端口隔离 |
| 测试便利性 | 需要 Mock 数据库 | 可直接测试用例 |
| 框架依赖 | 通常强依赖框架 | 框架无关 |
六边形架构(Alistair Cockburn 提出)与 Clean Architecture 本质上是同一思想的两种表述:
差异在于:六边形架构更强调"对称性"——左侧入口适配器(用户发起)和右侧出口适配器(系统发起)是对称的。Clean Architecture 更强调依赖方向的层级性。
Jeffrey Palermo 提出的洋葱架构与 Clean Architecture 几乎一致:
可以认为这三种架构(Clean、Hexagonal、Onion)解决的是同一个问题——如何保护核心业务逻辑不受外部变化的影响。它们在不同的时期、由不同的人独立提出,最终殊途同归。
阿里巴巴的 COLA 架构是 Clean Architecture 在企业级 Java 应用中的实现框架:
以一个订单管理系统为例,推荐的目录结构:
order-system/
├── domain/ # 实体层
│ ├── entity/
│ │ ├── Order.java
│ │ ├── OrderItem.java
│ │ └── User.java
│ └── vo/ # 值对象
│ ├── Money.java
│ └── Address.java
├── application/ # 用例层
│ ├── port/ # 端口接口
│ │ ├── OrderRepository.java
│ │ └── PaymentService.java
│ ├── usecase/
│ │ ├── CreateOrderUseCase.java
│ │ ├── CancelOrderUseCase.java
│ │ └── GetOrderUseCase.java
│ └── dto/
│ ├── CreateOrderRequest.java
│ ├── CreateOrderResponse.java
│ └── OrderDTO.java
├── adapter/ # 接口适配器层
│ ├── web/
│ │ ├── OrderController.java
│ │ └── OrderDTOMapper.java
│ ├── persistence/
│ │ ├── JpaOrderRepository.java
│ │ └── entity/
│ │ └── OrderJpaEntity.java
│ └── messaging/
│ └── OrderMessagePublisher.java
├── bootstrap/ # 启动配置层
│ ├── Application.java
│ ├── BeansConfiguration.java
│ └── application.yml
└── build.gradle
第一阶段:定义业务边界
在写代码之前,先定义清晰的业务边界:
订单管理边界:创建订单 → 支付 → 发货 → 确认收货
用户管理边界:注册 → 登录 → 更新信息
商品管理边界:上架 → 下架 → 更新库存
每个边界对应一个或多个 Use Case。
第二阶段:从 Use Case 开始
不要从数据库设计开始,从 Use Case 开始:
第三阶段:选择合适的依赖注入方式
在 Clean Architecture 中,事务是一个典型的"横切关注点"。
推荐做法:
// ✅ 推荐的:在 Use Case 级别管理事务
public class CreateOrderUseCase {
@Transactional // 事务注解在用例层
public CreateOrderResponse execute(CreateOrderRequest request) {
// ...
}
}
症状:Entity 中没有任何业务方法,只有 getter/setter,所有的业务逻辑都在 Service 中。
// ❌ 贫血模型
@Entity
public class Order {
@Id private Long id;
private String status;
// 只有 getter/setter,没有业务方法
public void setId(Long id) { this.id = id; }
public Long getId() { return id; }
public void setStatus(String status) { this.status = status; }
public String getStatus() { return status; }
}
// Service 中充满了本该属于 Order 的业务逻辑
@Service
public class OrderService {
public void completeOrder(Long orderId) {
Order order = repository.findById(orderId);
if ("COMPLETED".equals(order.getStatus())) {
throw new RuntimeException("Order already completed");
}
order.setStatus("COMPLETED");
repository.save(order);
}
}
解决方案:将业务逻辑移到实体中。
// ✅ 充血模型
public class Order {
private String id;
private OrderStatus status;
public void complete() {
if (status == OrderStatus.COMPLETED) {
throw new IllegalStateException("Order already completed");
}
this.status = OrderStatus.COMPLETED;
}
}
// Use Case
public class CompleteOrderUseCase {
public void execute(String orderId) {
Order order = repository.findById(orderId);
order.complete(); // 业务逻辑在实体中
repository.save(order);
}
}
症状:为了"完美"的 Clean Architecture,创建了大量不必要的抽象层。
识别标志:
解决方案:
症状:为了让 Entity 可以在网络间传输,在 Entity 中添加了 JSON 注解或实现了 Serializable 接口。
// ❌ 实体层不应该知道序列化
public class Order {
@JsonProperty("order_id")
private String id;
@JsonIgnore
private List<OrderItem> items;
}
解决方案:适配器层负责序列化和反序列化,实体保持纯净。
根据我的项目经验,Use Case 的粒度是 Clean Architecture 中最难把握的点:
经验法则:
问题:把数据库事务当作业务事务使用。
例如,创单用例中包含了:创建订单 → 扣减库存 → 发送通知。如果发送通知失败,整个事务回滚,订单被取消——这是不合理的。
解决方案:
经过多个项目的实践,我的判断是:
适合的场景:
不适合的场景:
不要一上来就"架构大爆炸"。推荐的做法是:
架构是长出来的,不是设计出来的。——这个观点我深表赞同。好的架构像一棵树,根(Entity)最稳定,越往外层变化越频繁。
在中国互联网的"快节奏"工作环境下,实践 Clean Architecture 面临一些现实挑战:
应对策略:
Clean Architecture 和 DDD(领域驱动设计)是互补关系,而非竞争关系:
两者的结合是"用 DDD 建模,用 Clean Architecture 保护模型"。这是我在实际项目中最推崇的组合。
Clean Architecture 的价值不仅在于具体的代码组织方式,更在于它传达的一种思维方式:好的架构应该让核心业务逻辑成为整个系统最稳定、最清晰、最容易被测试的部分。
在实际项目中,不必追求"百分之百的 Clean",关键在于让核心业务逻辑与外部细节解耦。一个系统是不是"整洁",判断标准很简单——当你需要理解系统如何工作时,你只需要看 Entity 和 Use Case 就能搞清楚业务是全貌吗?如果答案是肯定的,这就是一个成功的 Clean Architecture 实践。
💡 个人体会:Clean Architecture 最大的价值不在于教你怎么写代码,而在于教你怎么思考代码的组织方式。它提供了一种心智模型,让你在做架构决策时有一个"北极星"来指引方向。
📚 架构师书架系列 | 基于《Clean Architecture: A Craftsman's Guide to Software Structure and Design》[Robert C. Martin, 电子工业出版社] 整理