Clean Architecture(Robert C. Martin 2017 年提出)、六边形架构(Hexagonal,Alistair Cockburn 提出)、洋葱架构(Onion,Jeffrey Palermo)—— 这三种架构思路高度相似,核心都是"业务逻辑独立于技术细节"。这篇文章把它们讲透,用一个完整的例子展示如何应用。
这些架构要解决什么
传统三层架构(Controller → Service → Repository)的痛点:
- Service 直接 import 数据库 ORM,业务和持久化耦合。
- Controller 依赖具体框架(Spring / Express),业务跟着框架走。
- 换数据库 / 框架 / UI = 重写。
- 测试要起整套基础设施。
Clean Architecture 的核心规则:依赖方向只能从外向内。
同心圆架构
外层 (框架、数据库、UI、外部 API)
/
接口适配层 (Controller、Repository 实现、Gateway)
/
用例层 (Application Services、Use Cases)
/
实体层 (Domain Entities、Value Objects、领域规则)
依赖方向:外 -> 内,内层完全不知道外层的存在
四层详解
1. 实体层(Entities)
最内层,纯业务规则。不依赖任何外部技术 —— 不知道有数据库,不知道有 HTTP,不知道有 UI。
// pure 领域代码,没有任何框架依赖
class Order {
private items: OrderItem[] = [];
constructor(public readonly id: string, public readonly userId: string) {}
addItem(product: Product, qty: number) {
if (qty <= 0) throw new Error('qty must > 0');
this.items.push({ product, qty });
}
total(): number {
return this.items.reduce((sum, i) => sum + i.product.price * i.qty, 0);
}
}
2. 用例层(Use Cases / Application)
编排实体完成一个业务用例。依赖实体 + 接口(下面会说),不依赖具体技术实现。
// 用例的接口(定义需要什么)
interface OrderRepository {
save(order: Order): Promise<void>;
findById(id: string): Promise<Order | null>;
}
interface PaymentGateway {
charge(amount: number): Promise<PaymentResult>;
}
// 用例本身
class PlaceOrderUseCase {
constructor(
private orderRepo: OrderRepository,
private payment: PaymentGateway,
) {}
async execute(input: PlaceOrderInput): Promise<PlaceOrderOutput> {
const order = new Order(generateId(), input.userId);
for (const item of input.items) {
order.addItem(item.product, item.qty);
}
const result = await this.payment.charge(order.total());
if (!result.success) throw new PaymentError(result.message);
await this.orderRepo.save(order);
return { orderId: order.id };
}
}
3. 接口适配层(Interface Adapters)
把外部世界(HTTP、数据库、消息队列)适配成内层认识的接口。
// 实现 OrderRepository 接口,用 MySQL
class MySQLOrderRepository implements OrderRepository {
constructor(private db: Database) {}
async save(order: Order) {
await this.db.query("INSERT INTO orders (id, user_id, total) VALUES (?, ?, ?)",
[order.id, order.userId, order.total()]);
}
async findById(id: string): Promise<Order | null> {
const row = await this.db.query("SELECT * FROM orders WHERE id = ?", [id]);
if (!row) return null;
return mapToOrder(row);
}
}
// HTTP Controller
class OrderController {
constructor(private placeOrderUC: PlaceOrderUseCase) {}
async handlePOST(req, res) {
try {
const result = await this.placeOrderUC.execute({
userId: req.user.id,
items: req.body.items,
});
res.json({ orderId: result.orderId });
} catch (e) {
res.status(400).json({ error: e.message });
}
}
}
4. 外层(Frameworks & Drivers)
Spring Boot、Express、Knex、Redis 客户端 —— 具体技术框架。在这一层 wire 起来。
// main.ts:启动入口
const db = new MySQLDatabase(config.db);
const orderRepo = new MySQLOrderRepository(db);
const payment = new StripePaymentGateway(config.stripe);
const placeOrderUC = new PlaceOrderUseCase(orderRepo, payment);
const orderController = new OrderController(placeOrderUC);
const app = express();
app.post('/orders', (req, res) => orderController.handlePOST(req, res));
app.listen(3000);
关键原则:依赖倒置
内层定义接口(Port),外层实现接口(Adapter)。这就是"端口与适配器架构"(Hexagonal)的命名来源。
用例 (Port: OrderRepository interface)
↑ 实现(依赖反过来)
MySQLOrderRepository (Adapter)
这种设计让:
- 用例可以无修改地切换数据库实现。
- 测试时用 InMemoryOrderRepository,无需起数据库。
- 业务规则的变化不会触发框架代码改动。
测试的天堂
describe('PlaceOrderUseCase', () => {
it('places order successfully', async () => {
// 全部用内存实现,无任何外部依赖
const orderRepo = new InMemoryOrderRepository();
const payment = new FakePaymentGateway({ success: true });
const uc = new PlaceOrderUseCase(orderRepo, payment);
const result = await uc.execute({
userId: 'u1',
items: [{ product: testProduct, qty: 2 }],
});
expect(result.orderId).toBeDefined();
expect(orderRepo.findById(result.orderId)).resolves.toBeTruthy();
});
it('rejects when payment fails', async () => {
const uc = new PlaceOrderUseCase(
new InMemoryOrderRepository(),
new FakePaymentGateway({ success: false, message: 'card declined' }),
);
await expect(uc.execute(...)).rejects.toThrow('card declined');
});
});
// 单元测试覆盖全部业务逻辑,毫秒级跑完
目录结构示例
src/
├── domain/ # 实体层
│ ├── Order.ts
│ ├── OrderItem.ts
│ └── Money.ts
├── application/ # 用例层
│ ├── PlaceOrderUseCase.ts
│ ├── CancelOrderUseCase.ts
│ ├── ports/ # 内层定义的接口
│ │ ├── OrderRepository.ts
│ │ └── PaymentGateway.ts
│ └── dto/ # 输入输出 DTO
├── infrastructure/ # 接口适配层(外向内)
│ ├── persistence/
│ │ └── MySQLOrderRepository.ts # 实现 OrderRepository 接口
│ ├── payment/
│ │ └── StripePaymentGateway.ts # 实现 PaymentGateway 接口
│ └── http/
│ └── OrderController.ts
└── main.ts # wire 所有依赖
过度工程的危险
Clean Architecture 不是免费的:
- 代码量增加 30-50%(接口 + 实现 + 映射)。
- 开发简单功能要写多个文件。
- 团队学习曲线陡。
何时值得:
- 项目大且长期演化(几年以上)。
- 有可能换数据库 / 框架。
- 需要严格的可测试性。
- 团队有架构能力贯彻执行。
何时不值得:
- 3 个月的项目。
- 简单 CRUD 管理后台。
- 团队没人懂这套思路。
常见误用
误用 1:每一层都做映射。Entity → DTO → ResponseDTO → ViewModel,5 次映射,代码 80% 在搬数据。简化:同一层内能用一个类就用一个,跨层用边界 DTO 即可。
误用 2:用例做得过细。每个 HTTP endpoint 一个 UseCase 类。小项目里直接在 Controller 里写也行,UseCase 只在逻辑复杂时抽。
误用 3:把所有"代码组织"称作 Clean Architecture。Clean Architecture 的内核是"依赖方向规则",不是"分多少层"。守住依赖规则,层多少都可以。
写在最后
Clean Architecture 是"用代码组织保护业务规则不被技术细节污染"的设计思想。它不是金科玉律,要根据项目规模选用。新人接触时容易"为架构而架构",写一堆样板代码。资深团队的应用是有节制的 —— 在真正复杂的核心业务上严格分层,在简单 CRUD 上保持轻量。这种"按需架构"的判断力,是工程师从中级到高级的标志。
一图看懂
Clean Architecture 同心圆一图看懂:
—— 别看了 · 2026