L7 - 战术深入:领域服务 / 工厂 / 仓储 / 模块
BC 内的代码组织——跨实体行为怎么放?复杂创建怎么管?聚合怎么持久化?模块怎么分?
一句话
领域服务 = 跨实体行为;工厂 = 复杂创建;仓储 = 聚合持久化;模块 = BC 内代码组织——4 个战术工具,覆盖 BC 内除聚合外的所有代码组织。
这本书在这一层说了什么
本层融合 Vernon 4 章(IDDD Ch 7 + Ch 9 + Ch 11 + Ch 12),共 35KB / 22K 词。
Vernon Ch 7 开篇:
"Sometimes, it just isn't a thing." —Eric Evans
Vernon Ch 9 开篇:
"The secret of all victory lies in the organization of the non-obvious." —Marcus Aurelius
Vernon Ch 11 开篇:
"Of all the patterns used in DDD, Factory is probably one of the better known."
Vernon Ch 12 开篇:
"A repository commonly refers to a storage location, usually considered a place of safety or preservation of the items stored in it."
核心论点 4 个:
- Domain Service(Ch 7)——跨实体行为 + 无状态的领域操作
- Module(Ch 9)——BC 内的命名容器 + 按业务能力分包
- Factory(Ch 11)——复杂聚合创建的责任转移
- Repository(Ch 12)——聚合持久化的集合式抽象
是什么
1. Domain Service(领域服务)
Vernon Ch 7 关键定义:
"A Service in the domain is a stateless operation that fulfills a domain-specific task. Often the best indication that you should create a Service in the domain model is when the operation you need to perform feels out of place as a method on an Aggregate or a Value Object."
Vernon Ch 7 三层服务分类:
- Domain Service(领域服务)——纯领域逻辑,跨实体
- Application Service(应用服务)——用例编排,事务管理
- Infrastructure Service(基础设施服务)——技术能力封装(邮件 / 文件 / RPC)
Vernon Ch 7 关键判断标准:
"If the operation you need to perform feels out of place as a method on an Aggregate or Value Object, that's a signal to use a Domain Service. However, when using DDD, that tactic is a code smell that likely indicates you need a Service instead."
——警惕"在聚合根上放静态方法"——这是反模式。
特征:
- 无状态——不持有领域状态
- 接口优先——以领域语言命名(不是技术命名)
- 无副作用或副作用明确——领域服务可修改聚合
- 跨聚合边界操作——单一聚合无法承担
何时用:
- 跨聚合的业务流程(如"转账"涉及双方账户)
- 复杂计算(业务规则密集,不适合放在实体上)
- 与外部领域模型的交互(跨 BC 翻译)
何时不用:
- 行为可以自然放聚合根 → 用实体方法
- 行为是技术性(事务 / 安全) → 用应用服务
- 行为是 CRUD 简单读写 → 直接用仓储
Vernon Ch 7 反模式警告:
"As a rule of thumb, we should try to avoid the use of Repositories from inside Aggregates, if at all possible."
——不要在聚合根里注入仓储。
Vernon Ch 7 命名约定:
- 领域服务命名用动词短语(TransferMoneyService)
- 接口与实现分离(接口在领域层,实现在基础设施层)
- 服务应该是无副作用或明确副作用
2. Module(模块)
Vernon Ch 9 关键定义:
"Modules in your model serve as named containers for domain object classes that are highly cohesive with one another. The goal should be low coupling between the classes that are in different Modules."
Vernon Ch 9 关键洞察:
"Since Modules as used in DDD are not bland or generic groupings of like kinds of things, but rather actual manifestations of the Ubiquitous Language..."
——模块不是技术分组(不是"Controller/Service/Repository"包),而是 Ubiquitous Language 的物理映射。
两种模块化:
- Traditional Module(传统模块)——代码包 / 命名空间 / 文件夹
- Deployment Modularity(部署模块)——独立部署单元(如独立 JAR / 微服务)
Vernon Ch 9 关键洞察:
"Designing Modules mechanically actually stifles modeling creativity."
——机械按技术分层(如按 MVC 三层分包)会扼杀领域建模的创造力。
Vernon Ch 9 命名原则:
- 用 Ubiquitous Language 命名——不是 "Service" "Impl"
- 按业务能力分包——"Billing" "Shipping" "Identity" 而不是 "Controller" "Service"
- 模块边界与 BC 边界对齐——一个 BC 可以有多个模块,但模块不应跨 BC
Vernon Ch 9 模块 vs BC 边界:
"When you should favor new Modules over new Bounded Contexts?"
| 选择标准 | 新 BC | 新 Module |
|---|---|---|
| 业务概念清晰独立 | ✅ | ❌ |
| 需要独立部署 | ✅ | ❌ |
| 需要独立团队 | ✅ | ❌ |
| 需要独立演化节奏 | ✅ | ❌ |
| 仅是代码组织 | ❌ | ✅ |
| 仅是命名清晰 | ❌ | ✅ |
Vernon Ch 9 反模式:
- 按技术层分包(所有 Service 放 service 包)——扼杀领域语言
- 按数据库表分包(OrderDao / UserDao)——技术视角不是业务视角
- 按团队分包——除非团队边界 ≈ BC 边界,否则会带来协调成本
Vernon Ch 9 模块凝聚力原则:
"Modules should have low coupling and high cohesion"
- 低耦合——模块间依赖最小化
- 高内聚——模块内的类紧密相关
3. Factory(工厂)
Vernon Ch 11 关键定义:
"Shift the responsibility for creating instances of complex objects and AGGREGATES to a separate object, which may itself have no responsibility in the domain model but is still part of the domain design."
——把创建职责从领域对象转移到一个专门的对象。
Evans 经典定义(Vernon Ch 11 引用):
"Create entire AGGREGATES as a piece, enforcing their invariants."
——整个聚合作为一个整体创建,不变性在创建时就要满足。
Vernon Ch 11 何时用 Factory:
- 聚合创建复杂(含子实体 / 值对象)
- 创建逻辑需要业务规则
- 跨 BC 创建(从外部模型翻译为本地模型)
Vernon Ch 11 何时不用 Factory:
- 直接
new Entity()足够(简单对象) - 构造函数足够清晰(参数不复杂)
- 创建是机械的(无业务规则)
Factory 三种位置:
| 位置 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| 聚合根上的工厂方法 | 同 BC 内创建聚合 | 简单,自然 | 跨 BC 时需翻译 |
| 领域服务上的工厂方法 | 跨 BC 创建 + 翻译 | 集中翻译逻辑 | 增加服务类 |
| 单独的工厂类 | 创建逻辑复杂 | 解耦 | 增加类数量 |
Vernon Ch 11 关键反模式警告:
"Don't have public Aggregate constructors."
——聚合根构造函数应该是包内可见(Java protected / C# internal),通过工厂方法创建。
Vernon Ch 11 Factory 与聚合根的关系:
"Use Factory Methods as Aggregate behaviors"
——工厂方法本身是聚合根的行为,调用方无需知道创建细节。
4. Repository(仓储)
Evans 经典定义(Vernon Ch 12 引用):
"For each type of object that needs global access, create an object that can provide the illusion of an in-memory collection of all objects of that type. Set up access through a well-known global interface. Provide methods to add and remove objects. ... Provide methods that select objects based on some criteria and return fully instantiated objects or collections of objects whose attribute values meet the criteria. ... Provide repositories only for aggregates. ..."
Vernon Ch 12 关键原则:
- 一个聚合根 = 一个仓储(不是每个实体一个仓储)
- 仓储返回聚合根(不是内部实体)
- 仓储按 ID 加载(不是返回所有对象再过滤)
- 仓储只暴露聚合根的 CRUD(复杂查询用专门的 Query Service)
Vernon Ch 12 仓储 vs DAO:
- DAO(Data Access Object)——技术视角,对应一个表
- Repository——领域视角,对应一个聚合
- 一个聚合 = 多张表(聚合内有实体 / 值对象)——仓储一次性加载整个聚合
Vernon Ch 12 关键反模式:
"Don't expose internal Aggregate Entities to the outside world through the Repository. Only the Aggregate Root should be exposed."
——只暴露聚合根,不要暴露内部实体。
Vernon Ch 12 持久化无关性:
- 接口在领域层(
OrderRepository接口) - 实现在基础设施层(
JpaOrderRepository) - 持久化机制无关——可以换数据库而不改领域
Vernon Ch 12 Query Service 的边界:
- 仓储 = 按 ID 加载 + 简单查询
- 复杂查询(报表 / 跨聚合 JOIN) = 专门的 Query Service
- 仓储不要塞复杂查询方法
Vernon Ch 12 仓储的实现模式:
- Hibernate / JPA——最常用,Java
- Entity Framework——.NET
- Event Sourced Repository——事件溯源场景(参考 ch08)
- In-Memory Repository——测试场景
为什么
为什么需要领域服务?
根因 1:跨聚合行为
- 转账 = 借方账户修改 + 贷方账户修改
- 不能由单一聚合根承担
- 也不能拆成两个聚合根依次调用(破坏事务边界)
根因 2:业务流程的领域性
- 业务规则跨实体(涉及多个聚合)
- 不是单纯的技术编排(那是应用服务)
- 不是简单的数据访问(那是仓储)
根因 3:单一职责
- 聚合根 = 单一聚合的边界保护
- 领域服务 = 跨聚合行为的协调
- 应用服务 = 技术事务 / 安全 / 日志
为什么需要模块?
Vernon Ch 9 关键理由:
"Naming things well is one of the most important activities in software development. The quality of the names we choose is a primary indicator of the quality of the model."
——命名质量 = 模型质量的指示器。模块命名就是 Ubiquitous Language 的代码层映射。
根因 1:认知负载
- 单 BC 可能含数百个类
- 没有模块 = 找不到代码 = 改不动
- 模块 = 物理边界 = 认知地图
根因 2:Ubiquitous Language 的代码化
- 业务语言 = 团队交流语言 = 代码语言
- 模块名 = 业务概念(如 "Billing")
- 模块边界 = 业务边界
根因 3:包大小控制
- 大包 = 编译器慢 / IDE 卡
- 物理分包 = 性能 + 导航
为什么需要工厂?
Vernon Ch 11 关键理由:
"Create entire AGGREGATES as a piece, enforcing their invariants."
——聚合的不变性必须在创建时就满足。如果创建逻辑散落在调用方,不变性无法保证。
根因 1:聚合创建的复杂性
- 聚合 = 根实体 + 内部实体 + 值对象
- 创建时需要赋值 / 关联 / 计算初始状态
- 调用方不应该关心这些细节
根因 2:业务规则保护
- 创建时的业务规则(如订单创建时计算折扣)
- 调用方不知道规则 → 容易绕过
- 工厂封装规则 → 强制执行
根因 3:跨 BC 翻译
- 外部 BC 的对象不能直接成为本地聚合
- 翻译过程 = 业务规则
- 工厂承担翻译职责
为什么需要仓储?
Evans 经典比喻(Vernon Ch 12 引用):
"The illusion of an in-memory collection of all objects of that type."
——仓储 = 内存中集合的幻觉。业务代码像操作内存集合一样操作仓储。
根因 1:业务与持久化解耦
- 业务代码不应关心 SQL / 事务 / 数据库连接
- 仓储 = 业务视角的"集合"
- 换数据库 = 不改业务代码
根因 2:聚合完整性保护
- 直接 SELECT 子实体 → 绕过聚合根的不变性保护
- 仓储只暴露聚合根 → 强制走聚合根
根因 3:可测试性
- 测试时用 In-Memory Repository 替换真实数据库
- 不需要 DB 也能跑领域逻辑测试
根因 4:跨技术栈一致性
- Java 用 JPA / C# 用 EF / Python 用 SQLAlchemy
- 但业务代码都用 Repository 接口
- 切换技术栈 ≠ 切换业务代码
怎么做
Step 1:识别领域服务
// ❌ 反模式:在聚合根上放静态方法
public class Product extends ConcurrencySafeEntity {
public static BusinessPriorityTotals businessPriorityTotals(
Set<BacklogItem> aBacklogItems) {
// 静态方法不属于 Product 的领域职责
}
}
// ✅ 正模式:领域服务
public class DomainService {
public BusinessPriorityTotals businessPriorityTotals(
Set<BacklogItem> aBacklogItems) {
// 跨聚合业务逻辑
}
}Vernon Ch 7 流程:
- 识别"不属于任何聚合"的行为
- 创建领域服务接口(领域层)
- 领域服务实现(领域层)
- 应用服务编排(应用层)
Step 2:设计模块
Vernon Ch 9 决策树:
Q1: 是新的业务概念(独立演化)?
是 → 新 BC
否 → 新 Module
Q2: 需要独立部署?
是 → 新 BC
否 → 新 Module
Q3: 模块内是否高内聚 / 模块间是否低耦合?
否 → 重构现有模块边界Vernon Ch 9 命名规则:
- 包名 = 业务术语(
com.example.billing不是com.example.service) - 类名 = Ubiquitous Language(
Order不是OrderEntity) - 内部类按职责分组(
order.commandorder.query)
Step 3:实现工厂
Vernon Ch 11 聚合根上的工厂方法:
public class Order extends ConcurrencySafeEntity {
// 包内可见的构造函数
protected Order() {}
// 公开的工厂方法
public static Order create(
OrderId orderId,
Customer customer,
List<OrderItem> items) {
// 业务规则:必须至少一个商品
if (items.isEmpty()) {
throw new IllegalArgumentException("Order must have at least one item");
}
// 创建订单
Order order = new Order();
order.setId(orderId);
order.setCustomer(customer);
for (OrderItem item : items) {
order.addItem(item);
}
// 触发事件
order.registerEvent(new OrderCreated(orderId, customer, items));
return order;
}
}Vernon Ch 11 单独的工厂类(跨 BC 翻译):
public class OrderFromImportFactory {
public Order createOrder(ImportedOrder imported) {
// 翻译逻辑
OrderId orderId = new OrderId(imported.getId());
Customer customer = customerLookup.findByRef(imported.getCustomerRef());
List<OrderItem> items = mapItems(imported.getItems());
return Order.create(orderId, customer, items);
}
}Step 4:实现仓储
Vernon Ch 12 接口与实现分离:
// 领域层接口
public interface OrderRepository {
Order orderOfId(OrderId orderId);
void save(Order order);
void remove(Order order);
}
// 基础设施层实现
@Repository
public class JpaOrderRepository implements OrderRepository {
@PersistenceContext
private EntityManager entityManager;
@Override
public Order orderOfId(OrderId orderId) {
OrderJpaEntity jpa = entityManager.find(
OrderJpaEntity.class,
orderId.id()
);
return jpa == null ? null : jpa.toDomain();
}
@Override
public void save(Order order) {
OrderJpaEntity jpa = OrderJpaEntity.fromDomain(order);
entityManager.merge(jpa);
}
@Override
public void remove(Order order) {
OrderJpaEntity jpa = OrderJpaEntity.fromDomain(order);
entityManager.remove(jpa);
}
}Vernon Ch 12 关键提醒:
- 只暴露聚合根——不要暴露内部 Entity / Value Object
- 按 ID 加载——不要提供
findByXxx的复杂查询(用 Query Service) - 完整加载——不要 lazy loading 内部实体(破坏聚合边界)
Step 5:四者协同
// 应用层:编排(用领域服务 + 仓储 + 工厂)
public class OrderApplicationService {
@Autowired
private OrderRepository orderRepository;
@Autowired
private PricingService pricingService; // 领域服务
public OrderId placeOrder(PlaceOrderCommand cmd) {
// 1. 加载聚合
Order order = orderRepository.orderOfId(cmd.getOrderId());
// 2. 领域服务计算
Money price = pricingService.calculatePrice(order, cmd.getPromoCode());
// 3. 业务操作
order.applyDiscount(price);
// 4. 持久化
orderRepository.save(order);
// 5. 返回
return order.getId();
}
}什么时候用 / 什么时候不用
领域服务的 2 大场景
场景 1:跨聚合业务逻辑
- 转账(双方账户修改)
- 价格计算(订单 + 客户 + 促销)
- 库存分配(订单 + 库存 + 仓库)
场景 2:业务规则密集的计算
- 信用评分(多因子加权)
- 风险评估(多维度分析)
- 推荐算法(业务规则引擎)
不用的 3 大场景
不用 1:行为自然属于聚合根 → 放实体方法
Order.cancel()应该放在 Order 上,不在 OrderService 上
不用 2:行为是技术性 → 用应用服务
- 事务管理 / 安全授权 / 日志记录
不用 3:行为是 CRUD → 直接用仓储
findById不要包一层 Service
模块的 3 大场景
用模块 3:同 BC 内的代码分组(按业务能力)
不用模块:
- 一个 BC 太小(< 20 类)——单文件即可
- BC 是单一业务领域——按职责分包就够了
工厂的 2 大场景
用 1:聚合创建复杂(含子实体 / 计算 / 业务规则)
用 2:跨 BC 翻译(从外部模型到本地聚合)
不用:
- 创建简单(
new User("alice")足够) - 没有业务规则(构造函数即规则)
仓储的 2 大场景
用 1:聚合根的 CRUD 操作
用 2:按 ID 加载聚合
不用:
- 报表查询(用专门的 Query Service)
- 跨聚合 JOIN(用专门的数据访问组件)
- 内部实体查询(通过聚合根)
看不见的地方
失效模式 1:聚合根变成"上帝类"
症状:一个聚合根 1000+ 行,包含所有业务逻辑。
根因:把领域服务的行为堆到聚合根上。
修复:提取领域服务,把不属于聚合根的跨实体行为搬出去。
失效模式 2:模块按技术层分包
症状:com.example.controller / service / repository 分包。
根因:技术视角不是业务视角。
修复:按业务能力分包(com.example.billing / shipping / identity)。
失效模式 3:Factory 在构造函数里塞业务规则
症状:构造函数做业务校验、做外部调用、做持久化。
根因:把 Factory 当 Service 用。
修复:Factory 只负责创建,规则放领域服务 / 应用服务。
失效模式 4:仓储暴露内部实体
症状:OrderRepository.findItems(orderId) 返回 List<OrderItem>。
根因:绕过聚合根暴露内部实体。
修复:仓储只暴露聚合根,访问内部实体必须加载整个聚合。
失效模式 5:仓储变成 DAO
症状:仓储提供 findByXxx、joinXxx、groupByXxx。
根因:把 DAO 的模式搬到仓储。
修复:仓储保持简单(按 ID + 简单查询),复杂查询用 Query Service。
失效模式 6:仓储成为贫血 CRUD 工具
症状:业务逻辑都在应用服务里,仓储只是 SQL 包装。
根因:仓储没承担聚合完整性职责。
修复:仓储加载完整聚合,确保业务规则由聚合根执行。
失效模式 7:模块按数据库表分包
症状:com.example.dao.order / dao.user / dao.product。
根因:技术视角。
修复:模块 = 业务领域(如 billing.order / iam.user)。
相关节点
骨架 A — 世界观
- W2 涌现 - 行动(
/worldview/wonder-02-emergence-action)——领域服务 = 跨实体行动的载体 - W1 涌现 - 视野(
/worldview/wonder-01-emergence-vision)——模块 = Ubiquitous Language 的视野切片
骨架 B — 工程实践
- 价值流(
/engineering/value-stream)——工厂 + 仓储 + 服务 = 价值的物理流路径 - 变更管理(
/engineering/change-management)——模块边界 = 变更影响半径
与 EDM 对位
- E2 微服务基础(
/book-guides/event-driven/e2-microservice-basics)——模块边界 = BC 边界 = 服务边界 - E4 集成老系统(
/book-guides/event-driven/e4-integrating-legacy)——Factory 跨 BC 翻译 = EDM CDC + Outbox
与同书对位
- L3 BC(
/book-guides/iddd/l3-bounded-context)——BC = 模块的物理容器 - L5 实体 + 值对象(
/book-guides/iddd/l5-entity-value-object)——工厂创建 = 实体的入口 - L6 聚合(
/book-guides/iddd/l6-aggregate)——仓储加载聚合根 = 聚合边界的物理实现 - L8 跨 BC 集成(
/book-guides/iddd/l8-bc-integration)——跨 BC 翻译 = 应用服务的职责
状态
本层当前状态:mature。原书 4 章(Ch 7 + Ch 9 + Ch 11 + Ch 12)共 35KB / 22K 词已完整精读,按 V4 实践指引 9 段范本完整重写。
关键贡献:
- L7 从 stub(93 行)升级为 mature(582 行)
- 覆盖 4 大战术模式完整内容(Service / Factory / Repository / Module)
- 基于 Vernon Ch 7/9/11/12 原话引用 + 案例(SaasOvation)