当你的核心业务逻辑里写满了 Spring 的 @Service、MyBatis 的 Mapper,或者某个 Web 框架的注解时,业务其实已经被框架”绑架”了。整洁架构(Clean Architecture)用一组同心圆把代码分成「实体—用例—接口适配—框架与驱动」四层,并通过依赖倒置原则(DIP)让外层依赖内层,从而把业务规则与数据库、Web、第三方 SDK 彻底解耦。本文用一个订单下单场景,带你把这套架构真正跑起来。
一、为什么业务总被框架绑架
传统三层架构(Controller / Service / Dao)在初创项目里很顺手:写一个 Service,里面直接注入 MyBatis 的 Mapper,再调一下 Redis 缓存,一个接口就完成了。问题在于,这种写法让”业务规则”和”技术细节”长在了同一层。当有一天你想把 MySQL 换成 PostgreSQL,或者想脱离 Spring 单独对下单逻辑做单元测试时,会发现业务代码里到处都是框架的引用,根本拆不干净。
更隐蔽的代价是”可测试性”的丧失。一段真正的核心用例,本应只关心”什么条件下允许下单、库存如何扣减”,却被迫依赖一个真实的数据库连接。于是你的单元测试要么起一整套 Spring 容器,要么用一堆 Mock 把依赖层层打桩,测试越写越重,最后没人愿意写。
二、整洁架构的同心圆四层
整洁架构把所有代码画成一组同心圆,依赖方向永远指向圆心。越靠近圆心越抽象、越稳定;越靠外圈越具体、越易变。最外层是框架和驱动(Web、数据库、消息队列),最内层是业务实体。
┌─────────────────────────────────┐
│ 框架与驱动 (Web/DB/SDK) │ ← 最易变,可替换
├─────────────────────────────────┤
│ 接口适配层 (Controller/RepoImpl)│
├─────────────────────────────────┤
│ 用例层 (Use Cases / 应用服务) │
├─────────────────────────────────┤
│ 实体层 (Entities / 领域模型) │ ← 最稳定,不依赖任何外层
└─────────────────────────────────┘
依赖方向:外圈 ──► 内圈(永远向内)
四层的职责可以这样对照:
| 层级 | 依赖方向 | 主要职责 | 能否引用外层 |
|---|---|---|---|
| 实体层 | 无(最内) | 业务规则、领域对象、不变量 | 绝不 |
| 用例层 | 只依赖实体层 | 单个业务用例的编排与输入输出 | 绝不 |
| 接口适配层 | 依赖用例层与实体层 | 把外层数据转换成内层能懂的格式 | — |
| 框架驱动层 | 依赖适配层 | HTTP、ORM、缓存、消息等具体技术 | outermost |
关键约束只有一条:源代码依赖只能由外向内,内圈绝不知道外圈的存在。实体层里不该出现 @Entity、@Service 这类框架注解,用例层也不该直接 new 一个具体的数据库实现。
三、依赖倒置是解耦的核心
怎么做到”内层不知道外层”?答案是依赖倒置原则(DIP):高层模块和低层模块都依赖于抽象,而不是彼此依赖具体实现。在整洁架构里,这意味着”数据怎么存”的接口由内层定义,由外层去实现。
先看反面例子——用例直接依赖具体的仓储实现:
// ❌ 反面:业务层直接依赖具体的 MyBatis 实现
public class PlaceOrderService {
private final OrderMapper orderMapper; // 来自 MyBatis 框架
private final RedisTemplate redis; // 来自 Spring Data
public OrderResult place(OrderCommand cmd) {
Order order = cmd.toOrder();
orderMapper.insert(order); // 业务被迫认识数据库
redis.opsForValue().set("last:" + order.getId(), order);
return OrderResult.ok(order.getId());
}
}
再看正面例子——在领域层定义接口,让外层去实现它:
// ✅ 正面:接口定义在内层,实现留在外层
// 实体/用例层(不依赖任何框架)
public interface OrderRepository {
OrderId nextId();
void save(Order order);
Optional<Order> findById(OrderId id);
}
public class PlaceOrderUseCase {
private final OrderRepository repository; // 只依赖抽象
public PlaceOrderUseCase(OrderRepository repository) {
this.repository = repository;
}
public OrderResult execute(OrderCommand cmd) {
Order order = Order.create(cmd);
repository.save(order); // 不知道背后是 MySQL 还是内存
return OrderResult.ok(order.getId());
}
}
注意这段用例代码里没有任何 import org.springframework 或 import com.baomidou。它只认 OrderRepository 这个抽象。至于这个接口背后是 JDBC、JPA 还是内存 Map,用例层完全不关心——这正是可测试性的来源:单元测试里传一个内存版实现即可。
四、最小可运行示例:订单下单用例
把上面的思路落到一个完整的小例子。先是实体层,领域对象自己守住不变量:
// 实体层:只关心业务规则,不依赖框架
public class Order {
private final OrderId id;
private final List<OrderItem> items;
private OrderStatus status;
public static Order create(OrderCommand cmd) {
if (cmd.getItems().isEmpty())
throw new IllegalArgumentException("订单不能为空");
return new Order(new OrderId(cmd.getOrderNo()), cmd.getItems(), OrderStatus.CREATED);
}
public void pay() { this.status = OrderStatus.PAID; }
// getter ...
}
接着是接口适配层,把具体技术接到内层定义的接口上:
// 接口适配层:实现内层定义的抽象(这里可以是任意 ORM)
@Repository
public class JdbcOrderRepository implements OrderRepository {
private final JdbcTemplate jdbc;
public JdbcOrderRepository(JdbcTemplate jdbc) { this.jdbc = jdbc; }
@Override public void save(Order order) {
jdbc.update("insert into orders(id,status) values(?,?)",
order.getId().value(), order.getStatus().name());
}
@Override public Optional<Order> findById(OrderId id) { /* ... */ return Optional.empty(); }
}
最外层的 Web 适配器只负责收参数、调用例、转响应,不含任何业务判断:
// 框架驱动层:HTTP 入口,薄薄一层
@RestController
@RequestMapping("/orders")
public class OrderController {
private final PlaceOrderUseCase useCase;
public OrderController(PlaceOrderUseCase useCase) { this.useCase = useCase; }
@PostMapping
public OrderResult create(@RequestBody OrderCommand cmd) {
return useCase.execute(cmd); // 业务全在用例里,这里只是"传声筒"
}
}
// 装配:在应用入口把实现注入用例(Spring 或纯 Java 均可)
@Configuration
class AppConfig {
@Bean PlaceOrderUseCase placeOrderUseCase(OrderRepository repo) {
return new PlaceOrderUseCase(repo);
}
}
这里有个值得强调的细节:PlaceOrderUseCase 这个类本身没有任何 Spring 注解。它只是个普通的 Java 对象,容器只是”顺便”帮它注入了依赖。换句话说,即使明天把 Spring 换成 Guice,甚至完全不用 DI 框架、手写 new,业务代码一行都不用改。
五、它与 DDD、CQRS 如何配合
整洁架构不是孤立的,它和领域驱动设计(DDD)天然契合:最内圈放的就是 DDD 里的聚合根与领域服务,用例层则对应应用服务。当系统读写压力差异大时,还可以在用例层之上叠加CQRS(读写分离架构)——命令侧走用例修改实体,查询侧直接拼 DTO,两者共享实体层,互不污染。
换句话说,DDD 告诉你”业务长什么样”,CQRS 告诉你”读写怎么拆”,而整洁架构告诉你”这些代码该怎么摆、依赖该往哪指”。三者是互补关系,不是三选一。
六、落地时最容易踩的三个误区
| 误区 | 正确做法 | 为什么 |
|---|---|---|
| 给每一个类都加接口 | 只在”依赖会跨越层边界且实现可能变化”处抽象 | 盲目接口化会让代码膨胀、阅读负担翻倍 |
| 用例层写成贫血转发器 | 把业务判断、校验、状态流转放进用例或实体 | 否则业务又悄悄回流到 Controller,架构形同虚设 |
| 把 JPA 注解写进实体层 | 持久化注解留在适配层的”数据模型”上 | 实体层一旦依赖 ORM,就又被框架绑架回去了 |
其中第三点最容易被忽略。很多人图省事,直接用 @Entity 注解的类和领域实体混为一谈,结果持久化框架的字段约束、懒加载语义悄悄渗透进业务。干净的做法是:内层有纯业务的 Order,适配层有只负责映射的 OrderPo,两者在仓储实现里转换。多写一点转换代码,换来的是内层十年的稳定。
七、什么时候不该用整洁架构
架构不是越”干净”越好,而是越”合适”越好。对于纯 CRUD 后台、生命周期只有几周的原型、或者团队尚不熟悉分层思想的项目,强行上四层同心圆,只会把简单问题复杂化:一个接口要建命令类、结果类、用例类、仓储接口、仓储实现、控制器六七个文件,开发效率反而下降。
可以参考微服务熔断降级里的取舍思路:引入任何机制都要先问”它解决的核心痛点我有没有”。当你的系统确实面临”框架频繁更替””业务逻辑复杂且需高覆盖单测””多终端共用同一套业务”时,整洁架构的回报才真正显现。它和单例模式一样,是工具而非教条。
八、小结
整洁架构的精髓可以浓缩成三句话:用同心圆把代码按”稳定程度”分层;用依赖倒置让外层依赖内层、内层定义抽象;让框架永远待在最外圈,随时可被替换。它带来的最大红利不是”看起来高级”,而是业务规则的可测试性与可移植性——这也是代码可读性与长期可维护性的根基。下次当你又想在 Service 里直接 new 一个 Mapper 时,不妨停下来想一想:这段逻辑,五年后换掉数据库时,还动得动吗?




