之前参与的一个澳洲金融项目,被客户吐槽我们架构做的过于“老旧”,希望我们用DDD的思想实现。我个人觉得node项目,不适合用DDD。不仅处处别扭而且代码复杂。不过这里我还是梳理一下在Node中如何实现DDD。方便大家快速理解DDD
问题:贫血模型的问题
先看原来的代码,典型贫血模型(Anemic Model):
// ❌ 贫血模型:数据和逻辑分离
class Order {
id: string;
userId: string;
items: OrderItem[];
status: string;
totalAmount: number;
}
class OrderService {
async cancelOrder(orderId: string) {
const order = await this.repo.findById(orderId);
// 业务逻辑散落在 Service 层
if (order.status === 'shipped') {
throw new Error('已发货订单不能取消');
}
// ...
order.status = 'cancelled';
await this.repo.save(order);
await this.refundService.refund(order);
await this.inventoryService.restore(order.items);
}
}假设三个月后,同样的取消逻辑出现在 5 个地方,每个地方规则略有不同,改一个功能要翻 10 个文件。
充血模型(Rich Domain Model):把逻辑塞回实体
DDD 的核心思想:让数据和行为待在一起。
// ✅ 充血模型:实体自己管理状态变更
class Order {
private constructor(
private id: string,
private status: OrderStatus,
// ...
) {}
// 业务逻辑封装在实体内
cancel(): void {
if (this.status === OrderStatus.Shipped) {
throw new DomainError('已发货订单不能取消');
}
this.status = OrderStatus.Cancelled;
}
}现在取消逻辑只在一个地方,其他代码调用 order.cancel() 就行。
下面我们不讲理论,直接上代码,看一个完整的 DDD 怎么用,以及他的问题。
DDD 四层架构
graph LR UI(" User Interface Layer <br/>「接口层」") UI_NOTE["💡 职责:接收 HTTP 请求、返回响应、参数校验、错误处理<br/>📦 代码:Controller、DTO、Middleware"] APP(" Application Layer <br/>「应用层」") APP_NOTE["💡 职责:编排用例流程、调度领域对象、事务管理、发布领域事件<br/>📦 代码:UseCase、Command、EventHandler"] DOMAIN(" Domain Layer <br/>「领域层」") DOMAIN_NOTE["💡 职责:核心业务逻辑、状态变更、业务规则校验「不依赖外部」<br/>📦 代码:Entity、Value Object、Domain Service、Domain Event"] INFRA(" Infrastructure Layer <br/>「基础设施层」") INFRA_NOTE["💡 职责:数据库访问、外部 API 调用、消息队列、缓存、文件存储<br/>📦 代码:Repository 实现、Mapper、HTTP Client、Queue Client"] UI --> APP APP --> DOMAIN DOMAIN -.->|依赖倒置| INFRA UI -.- UI_NOTE APP -.- APP_NOTE DOMAIN -.- DOMAIN_NOTE INFRA -.- INFRA_NOTE style UI fill:#dbeafe,stroke:#1e40af,stroke-width:3px style APP fill:#bfdbfe,stroke:#3b82f6,stroke-width:3px style DOMAIN fill:#fef3c7,stroke:#f59e0b,stroke-width:3px style INFRA fill:#f3f4f6,stroke:#6b7280,stroke-width:3px style UI_NOTE fill:#f0f9ff,stroke:#bae6fd,stroke-width:1px,text-align:left style APP_NOTE fill:#eff6ff,stroke:#bfdbfe,stroke-width:1px,text-align:left style DOMAIN_NOTE fill:#fffbeb,stroke:#fde68a,stroke-width:1px,text-align:left style INFRA_NOTE fill:#fafafa,stroke:#e5e7eb,stroke-width:1px,text-align:left
依赖方向:上层依赖下层,Domain Layer 是核心,不依赖任何外部(依赖倒置原则)。
1. User Interface Layer(接口层)
接收请求、返回响应,不写业务逻辑。
// presentation/controllers/OrderController.ts
class OrderController {
constructor(
private cancelOrderUseCase: CancelOrderUseCase
) {}
async cancelOrder(req: Request, res: Response) {
const { orderId } = req.params;
const userId = req.user.id;
try {
// 这里是useCase, 可以将复杂的逻辑, 拆分不同的use case中。
await this.cancelOrderUseCase.execute({ orderId, userId });
res.json({ success: true });
} catch (error) {
if (error instanceof DomainError) {
res.status(400).json({ error: error.message });
} else {
res.status(500).json({ error: 'Internal error' });
}
}
}
}2. Application Layer(应用层)
编排用例流程,调度 Domain 和 Infrastructure。
// application/use-cases/CancelOrderUseCase.ts
class CancelOrderUseCase {
constructor(
private orderRepo: IOrderRepository,
private paymentService: IPaymentService,
private inventoryService: IInventoryService,
private eventBus: IEventBus
) {}
async execute(command: CancelOrderCommand): Promise<void> {
// 1. 加载聚合根
const order = await this.orderRepo.findById(command.orderId);
if (!order) {
throw new NotFoundError('订单不存在');
}
// 2. 权限检查
if (order.userId !== command.userId) {
throw new ForbiddenError('无权操作');
}
// 3. 执行领域逻辑
order.cancel();
// 4. 持久化
await this.orderRepo.save(order);
// 5. 触发副作用(退款、恢复库存)
await this.paymentService.refund(order.id, order.totalAmount);
await this.inventoryService.restoreStock(order.items);
// 6. 发布领域事件
await this.eventBus.publish(new OrderCancelledEvent(order.id));
}
}3. Domain Layer(领域层)
核心业务逻辑,不依赖外部。
// domain/entities/Order.ts
export class Order {
private constructor(
private readonly id: OrderId,
private status: OrderStatus,
// ...
) {}
// 工厂方法:创建新订单
static create(userId: UserId, items: OrderItem[]): Order {
if (items.length === 0) {
throw new DomainError('订单至少包含一个商品');
}
return new Order(OrderId.generate(), userId, items, OrderStatus.Pending, new Date());
}
// 领域行为
cancel(): void {
if (!this.canBeCancelled()) {
throw new DomainError(`订单状态为 ${this.status},不能取消`);
}
this.status = OrderStatus.Cancelled;
}
private canBeCancelled(): boolean {
return this.status === OrderStatus.Pending || this.status === OrderStatus.Paid;
}
}
// domain/value-objects/Money.ts
export class Money {
private constructor(
private readonly amount: number,
private readonly currency: string
) {
if (amount < 0) throw new DomainError('金额不能为负数');
}
add(other: Money): Money {
if (this.currency !== other.currency) {
throw new DomainError('不同币种不能相加');
}
return new Money(this.amount + other.amount, this.currency);
}
// ...
}4. Infrastructure Layer(基础设施层)
数据库、外部 API、消息队列等实现。
// infrastructure/repositories/OrderRepository.ts
class OrderRepository implements IOrderRepository {
constructor(private prisma: PrismaClient) {}
async findById(id: string): Promise<Order | null> {
const raw = await this.prisma.order.findUnique({
where: { id },
include: { items: true }
});
if (!raw) return null;
// 将数据库记录转换为领域对象
return OrderMapper.toDomain(raw);
}
async save(order: Order): Promise<void> {
const raw = OrderMapper.toPersistence(order);
await this.prisma.order.upsert({ where: { id: raw.id }, create: raw, update: raw });
}
}
// infrastructure/mappers/OrderMapper.ts
class OrderMapper {
static toDomain(raw: any): Order {
// 从数据库格式转换为领域对象
return Order.reconstitute(
OrderId.from(raw.id),
UserId.from(raw.userId),
raw.items.map(OrderItemMapper.toDomain),
OrderStatus[raw.status],
// ...
);
}
static toPersistence(order: Order): any {
// 从领域对象转换为数据库格式
return {
id: order.id.value,
userId: order.userId.value,
status: order.status,
// ...
};
}
}核心概念实战
Entity vs Value Object
Entity(实体):有唯一标识,生命周期中可变。
// ✅ Entity:订单有 ID,状态会变
class Order {
private id: OrderId; // 唯一标识
private status: OrderStatus; // 可变状态
equals(other: Order): boolean {
return this.id.equals(other.id); // 通过 ID 判断相等
}
}
// ✅ Value Object:金额通过数值判断相等
class Money {
private readonly amount: number; // 不可变
equals(other: Money): boolean {
return this.amount === other.amount && this.currency === other.currency;
}
// 返回新对象,不修改自己
add(other: Money): Money {
return new Money(this.amount + other.amount, this.currency);
}
}判断标准:
- 需要追踪生命周期?→ Entity
- 只关心值本身?→ Value Object
- 示例:用户(Entity)、邮箱地址(Value Object)、订单(Entity)、收货地址(Value Object)
Aggregate(聚合根)
问题:Order 和 OrderItem 谁管谁?
// ❌ 错误:外部直接修改 OrderItem
const order = await repo.findById('123');
order.items[0].quantity = 10; // 绕过了 Order 的校验逻辑
await repo.save(order);解决:Order 是聚合根,OrderItem 是聚合内部对象。
// ✅ 正确:通过 Order 修改 Item
class Order {
private items: OrderItem[] = [];
addItem(productId: string, quantity: number, price: Money): void {
// Order 控制添加逻辑
if (this.status !== OrderStatus.Pending) {
throw new DomainError('只有待支付订单才能添加商品');
}
const existing = this.items.find(i => i.productId === productId);
if (existing) {
existing.increaseQuantity(quantity);
} else {
this.items.push(new OrderItem(productId, quantity, price));
}
}
}
// OrderItem 只能通过 Order 访问
class OrderItem {
constructor(
public readonly productId: string,
private quantity: number,
public readonly price: Money
) {}
// 包内可见,外部不能直接调用
increaseQuantity(amount: number): void {
this.quantity += amount;
}
}聚合边界划分原则:
- 一个聚合一个 Repository
- 聚合内部对象不暴露给外部
- 跨聚合通过 ID 引用,不直接持有对象
// ✅ 正确:Order 引用 User 的 ID
class Order {
private userId: UserId; // 只存 ID,不存 User 对象
}
// ❌ 错误:Order 直接持有 User
class Order {
private user: User; // 跨聚合边界了
}Repository 模式
Repository 是领域层和基础设施层的桥梁。
// domain/repositories/IOrderRepository.ts
export interface IOrderRepository {
findById(id: string): Promise<Order | null>;
findByUserId(userId: string): Promise<Order[]>;
save(order: Order): Promise<void>;
delete(id: string): Promise<void>;
}关键点:
- 接口定义在 Domain 层
- 实现在 Infrastructure 层
- 只暴露领域对象,不暴露数据库细节
// ❌ 错误:泄露数据库细节
interface IOrderRepository {
executeQuery(sql: string): Promise<any>;
getPrismaClient(): PrismaClient;
}
// ✅ 正确:只暴露领域操作
interface IOrderRepository {
findById(id: string): Promise<Order | null>;
save(order: Order): Promise<void>;
}Domain Service vs Application Service
Domain Service:处理多个实体协作的领域逻辑。
// domain/services/PricingService.ts
class PricingService {
calculateDiscount(order: Order, coupon: Coupon): Money {
// 复杂定价逻辑:涉及 Order 和 Coupon 两个聚合
if (!coupon.canApplyTo(order)) return Money.zero();
return coupon.type === 'percentage'
? order.totalAmount.multiply(coupon.value / 100)
: Money.from(coupon.value);
}
}
// application/use-cases/ApplyCouponUseCase.ts
class ApplyCouponUseCase {
constructor(
private orderRepo: IOrderRepository,
private pricingService: PricingService // 注入 Domain Service
) {}
async execute(command: ApplyCouponCommand): Promise<void> {
const order = await this.orderRepo.findById(command.orderId);
const coupon = await this.couponRepo.findByCode(command.couponCode);
// 调用 Domain Service
const discount = this.pricingService.calculateDiscount(order, coupon);
order.applyDiscount(discount);
await this.orderRepo.save(order);
}
}Domain Event(领域事件)
当订单取消时,需要退款、恢复库存、发送通知。不要在 order.cancel() 里直接调用这些服务,用事件解耦。
// domain/events/OrderCancelledEvent.ts
export class OrderCancelledEvent {
constructor(
public readonly orderId: string,
public readonly userId: string,
public readonly cancelledAt: Date
) {}
}
// domain/entities/Order.ts
class Order {
private domainEvents: DomainEvent[] = [];
cancel(): void {
if (!this.canBeCancelled()) throw new DomainError('订单不能取消');
this.status = OrderStatus.Cancelled;
// 记录领域事件
this.addDomainEvent(new OrderCancelledEvent(this.id.value, this.userId.value, new Date()));
}
getDomainEvents(): DomainEvent[] { return [...this.domainEvents]; }
clearDomainEvents(): void { this.domainEvents = []; }
private addDomainEvent(event: DomainEvent): void { this.domainEvents.push(event); }
}
// application/use-cases/CancelOrderUseCase.ts
class CancelOrderUseCase {
async execute(command: CancelOrderCommand): Promise<void> {
const order = await this.orderRepo.findById(command.orderId);
order.cancel();
await this.orderRepo.save(order);
// 发布事件
const events = order.getDomainEvents();
for (const event of events) {
await this.eventBus.publish(event);
}
order.clearDomainEvents();
}
}
// application/event-handlers/OrderCancelledHandler.ts
class OrderCancelledHandler {
async handle(event: OrderCancelledEvent): Promise<void> {
await this.paymentService.refund(event.orderId);
await this.inventoryService.restoreStock(event.orderId);
}
}完整目录结构
src/
├── presentation/ # User Interface Layer
│ ├── controllers/
│ ├── middlewares/
│ └── dtos/
├── application/ # Application Layer
│ ├── use-cases/
│ ├── event-handlers/
│ └── interfaces/ # 接口定义(Repository、外部服务)
├── domain/ # Domain Layer
│ ├── entities/
│ ├── value-objects/
│ ├── services/
│ ├── events/
│ └── errors/
└── infrastructure/ # Infrastructure Layer
├── repositories/
├── mappers/
├── external-services/
└── database/
DDD 的实际缺点
DDD 真的 那么好吗?
Node.js/Express 不适合 DDD
DDD 诞生于 Java 生态,天然契合强 OOP 语言。在 Node.js 里写 DDD,处处别扭。
// ❌ Node.js:class 语法像"后妈养的"
class Order {
private id: OrderId; // TypeScript 的 private 是编译时检查,运行时还是 public
private status: OrderStatus;
// ...
}Node.js 的问题:
- class 不是一等公民:JavaScript 本质是原型链,class 只是语法糖
- 没有真正的 private:运行时一切都是 public,
private只是 TypeScript 编译时检查 - 依赖注入复杂:Java 有 Spring,Node.js 要自己搭 DI 容器(InversifyJS/TypeDI 或者用 nestJS)
- ORM 不够成熟:Java 有 Hibernate,Node.js 的 TypeORM 性能差、Prisma 不返回 class instance
Node.js 更适合函数式风格:
// ✅ 函数式:更贴合 JavaScript 思维
type Order = {
id: string;
status: OrderStatus;
items: OrderItem[];
};
const cancelOrder = (order: Order): Result<Order, DomainError> => {
if (order.status === 'shipped') {
return err(new DomainError('已发货订单不能取消'));
}
return ok({ ...order, status: 'cancelled' });
};
// 使用
const order = await getOrder(orderId);
const result = cancelOrder(order);
if (result.isOk) {
await saveOrder(result.value);
}所以 Node.js 做 DDD 要付出额外成本,不如 Java 那样自然。
还是太复杂
团队新人看代码:「为什么一个取消订单要写 7 个文件?」
- OrderController.ts # 接收请求
- CancelOrderCommand.ts # DTO
- CancelOrderUseCase.ts # 用例
- Order.ts # 实体
- IOrderRepository.ts # 接口
- OrderRepository.ts # 实现
- OrderMapper.ts # 映射
贫血模型只要 2 个文件:Controller + Service。
过度设计风险
不是所有业务都需要 DDD。我们有个配置管理模块,就是 CRUD,用 DDD 写了 200 行,普通 MVC 只要 50 行。
// ❌ 过度设计:简单 CRUD 用 DDD
class ConfigEntity {
constructor(
private id: ConfigId,
private key: ConfigKey,
private value: ConfigValue
) {}
updateValue(newValue: string): void {
this.value = ConfigValue.from(newValue);
}
}
// ✅ 够用就好:简单场景用简单方案
const config = await prisma.config.update({
where: { key: 'theme' },
data: { value: 'dark' }
});聚合边界难划分
Order 和 Payment 是两个聚合还是一个?
// 方案 A:Order 聚合包含 Payment
class Order {
private payment: Payment; // Payment 是聚合内部对象
}
// 方案 B:Order 和 Payment 是两个聚合
class Order {
private paymentId: string; // 只存 ID,跨聚合引用
}我们最初选了方案 A,后来发现支付要接入第三方系统,改成方案 B 重构了一周。
性能问题
DDD 追求纯领域对象,但有时需要跨多个聚合查询。
// ❌ 低效:加载 100 个 Order 实体,再遍历计算
async getTopCustomers(): Promise<Customer[]> {
const orders = await this.orderRepo.findAll();
const grouped = groupBy(orders, o => o.userId);
return sortBy(grouped, g => g.totalAmount).slice(0, 10);
}CQRS 解决方案
DDD 纯粹主义者会说「用 CQRS(Command Query Responsibility Segregation,命令查询职责分离)」。核心思想:写操作用领域模型,读操作用查询模型。
// 写操作(Command):用完整的领域模型
class CancelOrderUseCase {
async execute(command: CancelOrderCommand): Promise<void> {
const order = await this.orderRepo.findById(command.orderId); // 加载完整聚合根
order.cancel(); // 领域逻辑
await this.orderRepo.save(order);
}
}
// 读操作(Query):直接查询数据库,返回 DTO
class GetTopCustomersQuery {
async execute(): Promise<TopCustomerDTO[]> {
// 直接写 SQL,不加载领域对象
return prisma.$queryRaw`
SELECT u.id, u.name, COUNT(o.id) as order_count, SUM(o.total_amount) as total_spent
FROM users u LEFT JOIN orders o ON u.id = o.user_id
WHERE o.status = 'completed'
GROUP BY u.id ORDER BY total_spent DESC LIMIT 10
`;
}
}CQRS 的目录结构:
src/
├── application/
│ ├── commands/ # 写操作
│ │ ├── CancelOrderCommand.ts
│ │ └── CancelOrderHandler.ts
│ └── queries/ # 读操作
│ ├── GetTopCustomersQuery.ts
│ └── GetOrderDetailQuery.ts
├── domain/ # 只用于写操作
│ └── entities/Order.ts
└── infrastructure/
├── repositories/ # 写操作用
│ └── OrderRepository.ts
└── query-services/ # 读操作用
└── OrderQueryService.ts
大量的Mapper胶水代码
每个实体都要写 toDomain 和 toPersistence,重复劳动。
class OrderMapper {
static toDomain(raw: any): Order { /* 50 行 */ }
static toPersistence(order: Order): any { /* 50 行 */ }
}
class OrderItemMapper {
static toDomain(raw: any): OrderItem { /* 30 行 */ }
static toPersistence(item: OrderItem): any { /* 30 行 */ }
}DDD Lite:简化DDD
既然DDD 太复杂了,我们能不能做一些简化,降低复杂度,只用 DDD 的核心部分?
- ✅ 充血模型(Entity 包含业务逻辑)
- ✅ Value Object(关键值对象)
- ✅ Repository 接口
- ❌ 不用 Application Service(直接在 Controller 里编排)
- ❌ 不用 Domain Event
- ❌ 不用复杂的 Mapper(Prisma 的 model 直接当 Entity 用, 需要扩展model, 默认 Prisma 是 贫血模型)
总结
DDD 不是银弹,它解决的是复杂业务建模的问题,DDD 适合的场景:
- ✅ 使用 Java/C# 这种强 OOP 语言
- ✅ 业务规则复杂(订单、库存、支付联动)
- ✅ 需要长期维护(不是一次性项目)
- ✅ 团队有学习意愿和 OOP 经验
代码是写给人看的,顺手比正确更重要, 不要为了 DDD 而 DDD,选择适合团队和项目的架构才是正道。