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

DDD(Domain-Driven Design,领域驱动设计)是 Eric Evans 2003 年的著作,影响深远但被滥用严重。"我们在做 DDD"经常意味着"我们建了一堆 Entity / VO / Service / Repository,但业务还是一团乱"。这篇文章把 DDD 的真正核心思想讲透 —— 战略层(限界上下文、统一语言)比战术层(Entity / VO)重要十倍。

DDD 解决什么

复杂业务系统的核心痛点:

  • 需求方说"订单状态"和开发理解的不一样,沟通成本巨大。
  • 系统大了之后,谁也说不清"当用户下单时,到底发生了什么"。
  • 不同业务模块逻辑互相纠缠,改一处影响一片。

DDD 给出的解法:让代码长得像业务,让模块边界清晰

战略设计:限界上下文(Bounded Context)

DDD 最重要的概念。"限界上下文"是一个明确边界内的领域模型 —— 在这个边界内,术语有明确含义,模型自洽。

例子:电商系统里"订单"在不同上下文意思不同:

  • 销售上下文:订单 = 客户下的单(总价、商品、用户)。
  • 履约上下文:订单 = 要发的货(物流地址、SKU 列表、包裹)。
  • 财务上下文:订单 = 一笔收入(税务、发票、对账)。

朴素做法:一个 Order 类承载所有字段 —— 几十个字段,谁都不能完整修改。DDD 做法:三个上下文各有自己的 Order 模型,通过 ID 关联。每个上下文里 Order 都是简单清晰的。

统一语言(Ubiquitous Language)

每个限界上下文里,业务方和开发用同一套术语。"订单"在销售上下文意味着什么 —— 业务、产品、开发、测试都用同一个定义。

代码里这意味着:类名、方法名、字段名要用业务术语,不要用"万能名词"。

// 不好:技术术语
class OrderManager {
    process(o) { ... }
    update(o, status) { ... }
}

// 好:业务术语
class SalesOrder {
    place() { ... }
    cancel(reason: CancellationReason) { ... }
    fulfillBy(warehouse: Warehouse) { ... }
}

上下文映射(Context Map)

多个限界上下文之间怎么协作?9 种关系:

  • 共享内核(Shared Kernel):两个上下文共享一小段领域模型。慎用。
  • 客户/供应商:上游为下游提供服务,下游提需求,上游有义务满足。
  • 遵奉者(Conformist):下游完全按上游模型,自己不再翻译。
  • 防腐层(ACL):下游把上游模型翻译成自己的领域语言,防"污染"。最常用。
  • 发布语言(Published Language):上游公开一个标准格式(如标准 JSON Schema),下游按这个对接。
  • 分道扬镳(Separate Ways):两个上下文没共同业务关系,完全独立。
  • 大泥球(Big Ball of Mud):上下文边界不清晰,混在一起。这是要避免的反模式。

战术设计:核心构件

Entity(实体)

有唯一标识、可变状态的对象。如 User、Order、Product。两个 ID 相同的 Entity 即使其他字段不同也是同一个。

class User {
    constructor(private id: UserId, private email: Email) {}
    changeEmail(newEmail: Email) {
        if (this.email.equals(newEmail)) return;
        this.email = newEmail;
        // 发领域事件
        DomainEvents.publish(new UserEmailChanged(this.id));
    }
}

Value Object(值对象)

没有标识,只看值。Email、Address、Money 都是。不可变,改一个字段返回新实例。

class Money {
    constructor(readonly amount: number, readonly currency: string) {}

    add(other: Money): Money {
        if (this.currency !== other.currency) throw new Error('currency mismatch');
        return new Money(this.amount + other.amount, this.currency);
    }

    equals(other: Money) {
        return this.amount === other.amount && this.currency === other.currency;
    }
}

值对象很重要 —— 它把业务约束封装到类型里。Money 不会"悄悄加错币种,因为 add 会抛异常。

Aggregate(聚合)

一组紧密相关的对象,有一个"聚合根"(Aggregate Root)作为外部访问入口。其他对象都通过 root 访问。

class Order {
    // 聚合根
    private items: OrderItem[] = [];
    constructor(private id: OrderId, private userId: UserId) {}

    addItem(product: Product, quantity: number) {
        if (this.isFinalized()) throw new Error('已下单,不能改');
        this.items.push(new OrderItem(product, quantity));
    }

    total(): Money {
        return this.items.reduce((sum, i) => sum.add(i.subtotal()), Money.zero('CNY'));
    }
}

// 外部代码:不能直接拿到 OrderItem 改它,必须通过 Order
const order = repo.findOrder(id);
order.addItem(product, 2);
repo.save(order);

聚合的核心价值:封装业务一致性。Order 内部的所有变更都保证"不变量"(如总价 = 各项之和)。

Repository(仓储)

把聚合根的持久化封装起来,业务代码看不到 SQL / ORM 细节。

interface OrderRepository {
    findById(id: OrderId): Order | null;
    save(order: Order): void;
    findPendingByUser(userId: UserId): Order[];
}

// 实现可以是 MySQL、MongoDB、内存(测试)
class MySQLOrderRepository implements OrderRepository { ... }

Domain Service(领域服务)

"不属于任何单个 Entity 或 VO"的业务逻辑。例:转账涉及两个账户,放哪个 Entity 都别扭,放领域服务。

class TransferService {
    transfer(from: Account, to: Account, amount: Money) {
        from.withdraw(amount);
        to.deposit(amount);
    }
}

Application Service(应用服务)

编排领域对象完成用例,不包含业务逻辑本身。它是"用例"的入口。

class PlaceOrderUseCase {
    constructor(
        private orderRepo: OrderRepository,
        private productRepo: ProductRepository,
        private paymentService: PaymentService,
    ) {}

    async execute(cmd: PlaceOrderCommand) {
        const products = await Promise.all(cmd.items.map(i => this.productRepo.find(i.productId)));
        const order = Order.create(cmd.userId, products);
        await this.paymentService.charge(order.total());
        await this.orderRepo.save(order);
    }
}

领域事件(Domain Event)

业务里发生的有意义的事件,用于触发后续动作。

class OrderPaid {
    constructor(readonly orderId: OrderId, readonly paidAt: Date) {}
}

// Order 内部
this.status = 'paid';
DomainEvents.publish(new OrderPaid(this.id, new Date()));

// 其他模块订阅(解耦)
on(OrderPaid, async (event) => {
    await inventoryService.deduct(event.orderId);
    await emailService.sendReceipt(event.orderId);
    await pointsService.addPoints(event.orderId);
});

领域事件让模块解耦 —— "下单"业务不需要知道有哪些下游关心它。这是从单体演化到事件驱动架构的基础。

常见反模式

反模式 1:贫血模型。Entity 只有 getter/setter,业务逻辑全堆在 Service 里。这不是 DDD,是"面向对象编程的反面"。Martin Fowler 专门撰文批评过。

反模式 2:聚合太大。一个 Order 聚合包含 Customer、Address、Items、Shipments、Payments... 加载时性能差,事务粒度大。规则:聚合要小,跨聚合用 ID 引用 + 最终一致。

反模式 3:CRUD 套 DDD 外壳。增删改查的简单管理后台用 DDD 是过度设计,Active Record 模式更合适。DDD 给"真正有业务复杂度"的系统用。

什么时候用 DDD

  • 业务复杂(几十个子领域、术语多)。
  • 领域专家深度参与开发(否则建不出统一语言)。
  • 系统长期演化(几年到几十年)。
  • 多团队协作。

什么时候不用

  • CRUD 管理后台。
  • 原型 / MVP。
  • 团队没人懂 DDD,且没培训计划。

写在最后

DDD 不是"用 Entity / VO / Repository 这套架构",而是"让代码长得像业务"的思想。战略层(限界上下文 + 统一语言)比战术层(代码结构)更重要 —— 你可以不用 Aggregate,但不能没有明确的领域边界和清晰的术语。这是从程序员到架构师的核心能力跃迁。

一图看懂

DDD 限界上下文 + 聚合一图看懂:

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

SOLID 原则完全指南:从五个字母到工程肌肉记忆

2026-5-15 17:38:32

技术教程

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

2026-5-15 17:43:37

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