Clean Architecture 完全指南:从依赖倒置到端口与适配器

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
声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理 邮箱1846861578@qq.com。
技术教程

DDD 领域驱动设计完全指南:从限界上下文到聚合的工程实践

2026-5-15 17:43:37

技术教程

代码重构完全指南:从识别坏味道到 Strangler Fig 大重构

2026-5-15 17:43:37

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索