整洁架构模式:Bob大叔的依赖法则与分层哲学

整洁架构(Clean Architecture)是 Robert C. Martin(我们圈内都叫他 Bob大叔)在 2012 年左右系统提出的一套架构思想。说实话,我第一次看到那篇博客时,第一反应是——这不就是分层架构的加强版吗?后来在几个大型项目中反复实践,才慢慢品出其中的味道。

整洁架构的核心,说白了就一句话:让业务逻辑独立于框架、UI、数据库和任何外部工具。你想想看,我们多少项目被 Spring 绑架、被 Hibernate 拖累、被前端框架牵着鼻子走?整洁架构就是要打破这种局面。

依赖规则:一切依赖向内

整洁架构最核心的规则,我称之为「洋葱依赖法则」。它规定:源代码的依赖关系只能从外层指向内层,内层对外层一无所知

举个例子,我在一个电商项目中遇到过这样的场景:业务层直接调用了 MyBatis 的 SqlSession。后来要换数据库,结果业务代码改得面目全非。这就是典型的依赖方向搞反了。

依赖规则的核心要点:
  • 内层定义接口,外层实现接口
  • 内层不引用外层的任何类、方法或变量
  • 外层可以依赖内层,但内层绝不能依赖外层
  • 跨层传递数据使用简单的数据结构(如 DTO 或基本类型)

这里有个容易踩的坑——依赖倒置不是依赖注入。我曾经见过一个团队,他们用 Spring 的 @Autowired 把 DAO 注入到领域服务里,就宣称自己实现了整洁架构。嗯,这其实是两码事。依赖注入只是手段,依赖倒置才是目的。

四层架构:从外到内的职责划分

Bob 大叔把系统分成四个圈层,从外到内依次是:

层级 名称 职责 典型内容
最外层 框架与驱动层 与外部世界的交互 Web 框架、数据库驱动、UI 框架
第二层 接口适配层 数据格式转换、接口适配 Controller、Presenter、Gateway 实现
第三层 用例层 编排业务逻辑、处理输入输出 UseCase、Interactor
最内层 实体层 核心业务规则 Entity、ValueObject、DomainService

我个人习惯把实体层和用例层合称为「业务核心」,因为它们才是系统真正值钱的部分。接口适配层和框架层说白了都是「可替换的零件」。

框架与驱动层 接口适配层 用例层 实体层 依赖方向(向内) Web框架 数据库驱动 UI框架 外部API Controller Presenter Gateway实现 CreateOrderUseCase ProcessPaymentUseCase Order Entity Customer Entity

用例层:业务编排的核心

用例层是整洁架构中最容易被误解的一层。很多人觉得它就是个 Service 层,其实不然。用例层负责的是特定业务场景的完整流程编排,它不关心数据怎么存、页面怎么展示,只关心「这件事该怎么做」。

我举个例子。假设我们要实现「用户下单」这个功能:

// Java 示例:用例层代码
public class CreateOrderUseCase {
    private final OrderRepository orderRepo;
    private final PaymentGateway paymentGateway;
    private final NotificationService notificationService;
    
    // 注意:这里依赖的都是接口,不是具体实现
    public CreateOrderUseCase(OrderRepository orderRepo,
                              PaymentGateway paymentGateway,
                              NotificationService notificationService) {
        this.orderRepo = orderRepo;
        this.paymentGateway = paymentGateway;
        this.notificationService = notificationService;
    }
    
    public OrderResponse execute(CreateOrderRequest request) {
        // 1. 验证请求
        Order order = Order.create(request.getUserId(), request.getItems());
        
        // 2. 处理支付
        PaymentResult payment = paymentGateway.charge(order.getTotal());
        if (!payment.isSuccess()) {
            throw new PaymentFailedException(payment.getError());
        }
        
        // 3. 保存订单
        orderRepo.save(order);
        
        // 4. 发送通知
        notificationService.sendOrderConfirmation(order);
        
        return new OrderResponse(order.getId(), order.getStatus());
    }
}

你看,这个用例类里没有任何框架注解,没有数据库操作,没有 HTTP 请求。它只做一件事:编排业务逻辑。这就是整洁架构的精髓——业务代码干干净净,不掺杂任何技术细节

我的个人习惯:每个用例类只暴露一个公共方法(execute 或 handle),这样调用方永远知道该做什么。而且用例类应该是无状态的,所有状态都通过参数传递。

接口适配层:数据转换的守门员

接口适配层的作用,说白了就是「翻译」。它把外层的数据格式转换成内层能理解的格式,反过来也一样。我见过太多项目把 JSON 解析、DTO 转换、异常处理全部混在 Controller 里,结果 Controller 比业务代码还复杂。

整洁架构的做法是:

  • Controller:接收 HTTP 请求,解析参数,调用用例层
  • Presenter:把用例层的返回结果转换成视图模型
  • Gateway 实现:把用例层的接口调用转换成具体的数据库操作或外部 API 调用
// C++ 示例:接口适配层的 Presenter
class OrderPresenter {
public:
    // 把用例层的输出转换成 JSON 响应
    JsonResponse present(const OrderResponse& response) {
        JsonResponse json;
        json["order_id"] = response.orderId;
        json["status"] = response.status;
        json["created_at"] = response.createdAt;
        
        if (response.status == "FAILED") {
            json["error"] = response.errorMessage;
            return JsonResponse(400, json);
        }
        
        return JsonResponse(200, json);
    }
};

这里有个关键点:Presenter 只做数据转换,不做业务判断。我曾经在一个项目中看到 Presenter 里写了 if-else 判断订单状态,这其实是业务逻辑,应该放在用例层或实体层。

实体层:最稳定的业务核心

实体层是系统的「压舱石」。它包含的是最通用、最稳定的业务规则,不依赖任何框架、数据库或外部服务。一个设计良好的实体,即使把整个系统换掉,它也能原封不动地搬过去。

举个例子,订单实体的核心规则:

// Java 示例:实体层
public class Order {
    private final String orderId;
    private final String userId;
    private final List<OrderItem> items;
    private OrderStatus status;
    private final LocalDateTime createdAt;
    
    // 工厂方法:确保订单创建时状态正确
    public static Order create(String userId, List<OrderItem> items) {
        if (userId == null || userId.isEmpty()) {
            throw new IllegalArgumentException("userId must not be empty");
        }
        if (items == null || items.isEmpty()) {
            throw new IllegalArgumentException("order must have at least one item");
        }
        
        return new Order(
            UUID.randomUUID().toString(),
            userId,
            items,
            OrderStatus.PENDING,
            LocalDateTime.now()
        );
    }
    
    // 业务方法:取消订单
    public void cancel() {
        if (this.status == OrderStatus.SHIPPED) {
            throw new IllegalStateException("cannot cancel shipped order");
        }
        this.status = OrderStatus.CANCELLED;
    }
    
    // 计算总价
    public Money getTotal() {
        return items.stream()
            .map(OrderItem::getSubtotal)
            .reduce(Money.ZERO, Money::add);
    }
}
我曾经踩过的坑:在实体里直接使用 JPA 注解(如 @Entity、@Table)。这样做虽然方便,但会让实体层依赖 Hibernate,一旦要换 ORM 框架,整个实体层都得重写。正确的做法是:实体层用纯 POJO,ORM 映射放在接口适配层的 Repository 实现中。

Bob 大叔的架构思想精髓

总结一下整洁架构的核心思想,我提炼成四个原则:

  1. 独立于框架:架构不应该依赖某个框架的存在。框架是工具,不是约束。
  2. 可测试性:业务逻辑可以在没有 UI、数据库、Web 服务器的情况下单独测试。
  3. 独立于 UI:UI 可以轻松改变,而不需要改变业务逻辑。Web 可以换成桌面,桌面可以换成移动端。
  4. 独立于数据库:你可以把 Oracle 换成 MySQL,或者把 MySQL 换成 MongoDB,业务逻辑纹丝不动。

你可能会问:这样做会不会太复杂?说实话,对于简单的 CRUD 项目,整洁架构确实有点「杀鸡用牛刀」。但一旦系统复杂度上来,或者业务逻辑频繁变化,整洁架构的价值就体现出来了。我参与过一个金融项目,业务规则几乎每个月都在变,但因为采用了整洁架构,每次改动都只影响用例层,数据库和 UI 基本不用动——这种体验,真的很爽。

一句话总结:整洁架构不是银弹,但它给了我们一种「让变化可控」的思维方式。依赖规则是骨架,分层是血肉,而 Bob 大叔的哲学,是灵魂。

公众号:蓝海资料掘金营,微信deep3321