领域驱动设计(DDD)核心概念
领域驱动设计(Domain-Driven Design,DDD)由 Eric Evans 在其经典著作中提出,是一套应对复杂业务建模的软件方法论。其核心在于:将业务领域的知识深度融入软件模型,而非让技术实现主导架构。本章将逐一拆解 DDD 的五大核心构造块:实体、值对象、聚合、领域事件与限界上下文,并揭示它们如何在 NestJS 限流熔断场景中协同落地。
1. 实体(Entity)
实体是具有唯一标识且生命周期持续变化的对象。两个实体即便所有属性相同,只要标识不同,就是不同的对象。在限流熔断场景中,RateLimiter 实例、CircuitBreaker 实例都是典型的实体——每个实例拥有唯一的名称或 ID,其状态(如令牌桶余量、熔断开关)随时间推移而改变。
| 特征 | 说明 | 限流熔断示例 |
|---|---|---|
| 唯一标识 | 通过 ID(如 UUID、业务主键)区分 | circuitBreakerId: "cb:payment:svc" |
| 可变状态 | 属性可被修改,状态随业务操作演进 | 令牌桶剩余令牌数、熔断器状态(CLOSED/OPEN/HALF_OPEN) |
| 生命周期 | 从创建到持久化再到销毁,全程可追踪 | 熔断器从初始化 → 熔断 → 半开 → 恢复 |
💡 实战提示:在 NestJS 中,实体通常以 TypeORM/Mongoose 的
@Entity() 或 @Schema() 装饰器标记,并配合 @PrimaryGeneratedColumn() 生成唯一标识。限流熔断的实体应放在 domain/entities/ 目录下,与基础设施层解耦。
2. 值对象(Value Object)
值对象通过属性值来定义自身,没有唯一标识,且通常是不可变的。两个值对象如果所有属性相同,则视为相等。在限流熔断领域,RateLimitConfig(包含窗口大小、最大请求数)、CircuitBreakerPolicy(包含失败阈值、超时时间)都是典型的值对象。
- 不可变性:一旦创建,属性不可修改;需要变更时创建新实例。
- 无标识:比较基于所有字段的相等性,而非 ID。
- 自包含行为:可封装校验逻辑,如
isValid()、withIncreasedThreshold()。
// 值对象示例:限流配置
export class RateLimitConfig {
constructor(
public readonly windowMs: number,
public readonly maxRequests: number,
) {
if (windowMs <= 0 || maxRequests <= 0) {
throw new Error('Invalid rate limit configuration');
}
}
equals(other: RateLimitConfig): boolean {
return this.windowMs === other.windowMs
&& this.maxRequests === other.maxRequests;
}
}
3. 聚合(Aggregate)
聚合是一组相关实体和值对象的集群,对外暴露一个聚合根作为唯一访问入口。聚合保证内部数据的一致性,所有外部操作必须通过聚合根进行。在限流熔断场景中,CircuitBreaker 聚合包含:
- 聚合根:
CircuitBreaker实体(唯一标识 + 状态) - 内部实体:
FailureRecord(记录每次失败调用) - 值对象:
CircuitBreakerPolicy(阈值、超时配置)
🎯 聚合边界原则:一次事务只修改一个聚合;聚合之间通过领域事件或仓储接口通信。在 NestJS 中,聚合根通常作为 TypeORM 的
@Entity(),内部实体使用 @OneToMany 关联,但对外只暴露聚合根的方法。
4. 领域事件(Domain Event)
领域事件是领域中发生的、业务人员关心的、具有业务意义的事件。它记录了“已经发生的事情”,通常以过去时态命名。在限流熔断场景中,关键领域事件包括:
| 事件名称 | 触发条件 | 携带数据 | 典型订阅者 |
|---|---|---|---|
CircuitBreakerOpened |
失败次数超过阈值 | breakerId, failureCount, openedAt | 告警服务、监控面板 |
CircuitBreakerHalfOpened |
超时时间到达,允许试探 | breakerId, halfOpenedAt | 负载均衡器、日志记录器 |
RateLimitExceeded |
请求数超过窗口限制 | clientId, windowMs, currentCount | 限流中间件、客户端通知 |
⚠️ 领域事件 vs 消息队列:领域事件是业务概念,属于领域层;消息队列是技术实现,属于基础设施层。在 NestJS 中,领域事件先由聚合根发布到内存中的
EventBus,再由应用层决定是否持久化到 RabbitMQ/Kafka。
// 领域事件定义
export class CircuitBreakerOpenedEvent {
constructor(
public readonly breakerId: string,
public readonly failureCount: number,
public readonly openedAt: Date = new Date(),
) {}
}
// 聚合根中发布事件
export class CircuitBreaker {
private events: DomainEvent[] = [];
recordFailure(): void {
this.failureCount++;
if (this.failureCount >= this.policy.failureThreshold) {
this.state = CircuitState.OPEN;
this.events.push(
new CircuitBreakerOpenedEvent(this.id, this.failureCount)
);
}
}
pullEvents(): DomainEvent[] {
const events = [...this.events];
this.events = [];
return events;
}
}
5. 限界上下文(Bounded Context)
限界上下文是 DDD 中最关键的战术模式之一。它定义了模型、术语和业务规则的明确边界。每个限界上下文内部有统一的语言(Ubiquitous Language),上下文之间通过上下文映射(Context Map)进行集成。
在限流熔断课程中,我们识别出三个核心限界上下文:
- 限流上下文(Rate Limiting Context):负责令牌桶/滑动窗口算法、配额管理、客户端速率控制。术语:令牌、窗口、配额、节流。
- 熔断上下文(Circuit Breaking Context):负责熔断状态机、失败计数、半开试探、健康检测。术语:熔断器、阈值、半开、恢复。
- 监控上下文(Monitoring Context):负责事件收集、指标聚合、告警触发、可视化。术语:指标、事件流、告警规则、仪表盘。
概念对比速查表
| 概念 | 有无唯一标识 | 是否可变 | 比较方式 | 限流熔断示例 |
|---|---|---|---|---|
| 实体 | 有 | 是 | 按 ID 比较 | CircuitBreaker 实例 |
| 值对象 | 无 | 否(不可变) | 按所有属性比较 | RateLimitConfig |
| 聚合 | 通过聚合根标识 | 通过聚合根方法变更 | 按聚合根 ID 比较 | CircuitBreaker + FailureRecord + Policy |
| 领域事件 | 有(事件 ID) | 否(已发生) | 按事件类型 + 时间戳 | CircuitBreakerOpened |
| 限界上下文 | 有(上下文名称) | 边界固定,内部模型可变 | 按上下文边界划分 | 限流上下文 vs 熔断上下文 |
📐 架构决策:在 NestJS 项目中,每个限界上下文对应一个独立的 NestJS 模块(如
RateLimitingModule、CircuitBreakingModule),模块内部按 DDD 分层组织:domain/(实体、值对象、领域事件)、application/(用例服务)、infrastructure/(仓储实现、事件总线适配器)。上下文之间通过 @Module({ imports: [...] }) 或消息队列进行集成,避免直接依赖内部模型。