第一部分:基石 —— 探寻对象创建的“为何”与“为何物”
在复杂的软件系统中,对象的创建远非调用一个构造函数那么简单。它是一个涉及业务规则、数据一致性和抽象封装的关键活动。在深入领域驱动设计(DDD)的世界之前,我们必须首先回顾那些为解决对象创建问题而生的经典模式,以此构建一个共同的理解基础。
第一章:重温创建型模式:GoF的视角
“四人帮”(Gang of Four, GoF)在其划时代的《设计模式》一书中,为我们提供了解决通用软件设计问题的蓝图 1。其中,创建型模式专注于处理对象的实例化过程,它们为我们即将展开的DDD工厂之旅提供了必要的语境。
1.1 工厂方法模式:延迟实例化
工厂方法模式(Factory Method Pattern)的核心思想是定义一个用于创建对象的接口,但将实际的实例化决策延迟到子类中 2。它通过调用一个“工厂方法”而非直接使用
new
关键字来创建对象,从而解决了在不必指定精确类的情况下创建对象的问题 3。
其主要优势在于灵活性和多态性。客户端代码仅与一个抽象的产品接口(Product)和一个抽象的创建者(Creator)交互,完全解耦了与具体产品(Concrete Product)的依赖关系 3。当系统需要引入新的产品类型时,只需添加一个新的具体产品类和一个对应的具体创建者子类,而无需修改任何现有的客户端代码。
例如,在一个物流管理应用中,我们可以定义一个抽象的 Logistics
类,它包含一个抽象的工厂方法 createTransport()
。然后,RoadLogistics
子类可以重写此方法以返回一个 Truck
对象,而 SeaLogistics
子类则返回一个 Ship
对象。客户端代码通过 Logistics
接口调用 createTransport()
,无需关心它得到的是卡车还是轮船,只需知道返回的对象都实现了共同的 Transport
接口并拥有 deliver()
方法即可 3。
1.2 抽象工厂模式:创建产品族
如果说工厂方法模式关注的是单个产品的创建,那么抽象工厂模式(Abstract Factory Pattern)则着眼于创建一系列相互关联或相互依赖的对象,即“产品族” 4。它提供一个接口,用于创建一系列相关对象,而无需指定它们的具体类 6。
此模式的关键在于确保产品族内部的一致性。例如,一个图形用户界面(GUI)工具包可能需要为不同的操作系统(如Windows和macOS)提供风格一致的控件。一个 GUIFactory
抽象工厂接口可以定义 createButton()
和 createCheckbox()
两个方法。WindowsFactory
实现会创建 WindowsButton
和 WindowsCheckbox
,而 MacFactory
则创建 MacButton
和 MacCheckbox
。应用程序在启动时选择一个具体的工厂实例,之后所有的控件创建都通过这个工厂完成,从而保证了UI风格的统一 4。抽象工厂通常通过组合和委托来实现,而不是继承 7。
1.3 富领域模型中的创建难题
在领域驱动设计中,我们追求的是“富领域模型”,即领域对象不仅包含数据,还封装了丰富的业务行为和规则 8。在这种背景下,直接使用构造函数(
new
操作符)创建对象会迅速暴露出其局限性。
随着业务逻辑的增长,对象的构造函数可能会变得异常臃肿。它可能需要大量的参数,其中包含复杂的验证逻辑和依赖关系。这将创建对象的复杂责任推给了客户端代码 2。客户端不仅需要知道要创建哪个具体的类,还必须了解如何正确地配置这个对象,以确保它处于一个有效的初始状态。这种做法不仅会导致代码重复,还可能暴露对象的内部结构,破坏封装,并使得客户端承担了它本不应关心的领域知识 10。这为我们引入DDD中一个更为强大的创建机制——工厂(Factory)——埋下了伏笔。
第二章:DDD中创建的特殊角色
从通用的设计模式转向领域驱动设计的特定语境,我们会发现对象的创建被赋予了更深层次的语义和责任。它不再仅仅是一个技术层面的实例化过程,而是守护领域完整性的第一道防线。
2.1 DDD战术模式简介
领域驱动设计(DDD)是一种专注于将软件模型与核心业务领域紧密对齐的设计方法 11。在其丰富的工具箱中,“战术设计”提供了一系列构建块(Building Blocks),用于在代码中精确地表达领域模型。这些构建块包括实体(Entity)、值对象(Value Object)、聚合(Aggregate)、仓储(Repository)、领域服务(Domain Service)以及我们即将深入探讨的工厂(Factory) 12。
在DDD中,工厂并非一个孤立的模式,而是战术设计工具集中一个不可或缺的、与其他模式紧密协作的组成部分。它的存在是为了支持DDD的核心原则,特别是那些与对象生命周期和数据一致性相关的原则。
2.2 不变量的概念:守护业务规则
不变量(Invariant)是DDD中的一个核心概念,它指的是在任何时候都必须为真(保持一致)的业务规则或条件 15。例如,“一个订单的总金额必须等于其所有订单项金额之和”,或者“一个账户的余额永远不能为负数”。
DDD强烈建议,系统中的任何对象实例在任何时候都不应该处于无效状态 15。这意味着,从对象被创建的那一刻起,直到它被销毁,其内部的所有不变量都必须得到满足。这一原则是理解DDD工厂必要性的基石。如果一个对象从诞生之初就必须是有效的,那么它的创建过程就必须是一个能够保证这种有效性的原子操作。
2.3 聚合:真正的一致性边界
为了管理复杂领域中的不变量,DDD引入了聚合(Aggregate)的概念。聚合是一个由多个领域对象(实体和值对象)组成的集群,这个集群在数据变更时被视为一个单一的单元 18。集群中会有一个特定的实体被称为聚合根(Aggregate Root)。聚合根是外部访问该聚合内所有对象的唯一入口,其核心职责是在任何操作中强制执行整个聚合内部的不变量 16。
聚合是DDD中的事务边界(Transactional Boundary) 16。对聚合的任何修改都必须通过聚合根来执行,并且整个操作必须是原子的:要么全部成功,要么全部失败,从而确保聚合在操作完成后作为一个整体仍然处于一致和有效的状态。例如,向订单(
Order
,聚合根)中添加一个订单项(OrderLine
)时,不仅要添加 OrderLine
对象,还必须同步更新 Order
的总金额。这两个变更必须在同一个事务中完成,以维护“总金额等于各分项之和”这个不变量。
2.4 为何简单的new
常常力不从心
现在,我们可以清晰地看到一条逻辑链,它解释了为什么DDD需要一个专门的工厂模式:
-
需求:DDD旨在对复杂的业务领域进行建模。
-
工具:为了管理这种复杂性并定义一致性边界,引入了**聚合(Aggregate)**的概念。
-
规则:聚合必须在任何时候都强制执行其内部的不变量(Invariants),这意味着它永远不能存在于一个无效的状态。
-
推论:因此,聚合的创建不能是一个零散的过程(例如,先创建聚合根,再逐个添加子实体,最后进行验证)。它必须是一个原子操作,一次性产生一个完整的、有效的聚合实例。
-
方案:对于具有复杂初始化逻辑和内部不变量的聚合来说,一个标准的构造函数(
new
)往往不足以胜任这种原子性的、确保有效性的创建任务 17。这便催生了DDD工厂。DDD工厂的首要目标,就是成为聚合初始有效状态的守护者,确保每一个聚合从诞生之初就严格遵守所有业务规则 20。
这条逻辑链清晰地揭示了DDD工厂存在的根本原因。它不是一个可有可无的模式选择,而是遵循聚合与不变量原则所带来的必然结果。
第二部分:核心概念 —— DDD工厂详解
理解了DDD工厂存在的“为什么”之后,我们现在可以深入其内部,详细定义它的职责、辨析它与其他模式的差异,并探讨其具体的实现方式。
第三章:定义DDD工厂
在DDD的语境下,工厂的职责被提升到了一个新的高度。它不再仅仅是技术层面的对象创建者,更是领域知识的承载者和业务规则的执行者。
3.1 核心职责:创建完整且有效的聚合
DDD工厂最核心、最根本的职责是:作为一个原子操作,创建出完整的聚合(Aggregate),并确保其在创建完成后处于一个完全有效的状态 21。这意味着工厂的产出物不仅仅是一个对象实例,而是一个承载了业务意义、满足所有内部不变量的、可信的领域概念 17。
当创建一个复杂的聚合时,例如一个“订单”,可能需要组合客户信息、产品列表、折扣规则、配送地址等多个部分。工厂的责任就是将所有这些输入信息整合起来,执行必要的业务验证,然后一次性地构造出包含聚合根(Order
)和所有内部实体(如 OrderLine
)及值对象(如 ShippingAddress
)的完整聚合。客户端代码只需调用工厂,而无需关心这个聚合内部复杂的组装过程 17。
3.2 封装复杂性与强制执行不变量
工厂的第二个关键职责是封装创建过程的复杂性 10。聚合的创建逻辑可能非常复杂,可能涉及以下步骤:
-
输入数据验证:检查传入的参数是否合法,例如,订单至少需要一个订单项 17。
-
与其他聚合或服务的交互:创建过程可能需要查询其他仓储(Repository)来验证信息(如客户是否存在且状态正常),或者调用外部服务来获取数据(如实时汇率)23。
-
内部计算与逻辑决策:根据输入和外部数据计算初始状态,如订单总价、应用的折扣等。
-
不变量检查:在最终实例化之前,确保所有业务规则(不变量)都得到满足,例如,“高级客户的单次下单金额不能超过一万美元” 22。
通过将这些复杂的逻辑全部封装在工厂内部,客户端被彻底解放出来。这完美地体现了单一职责原则(Single Responsibility Principle, SRP):聚合根负责其生命周期中的业务行为,而工厂则专门负责其创建时的业务逻辑 20。
3.3 辨析DDD工厂与GoF工厂
一个常见的混淆点在于DDD工厂与GoF中定义的工厂模式之间的关系。虽然它们都与对象创建有关,但其核心目标和应用层面存在显著差异。可以将DDD工厂视为GoF创建型模式在领域建模层面的一种“超级模式”或特定应用 24。GoF模式更多地是解决技术层面的灵活性和解耦问题,而DDD工厂则是一个领域层面的构造,其首要关注点是
语义的正确性和业务规则的完整性 20。
为了更清晰地辨析这些模式,下表从多个维度对它们进行了比较:
模式 (Pattern) | 主要目的 (Primary Purpose) | 关键职责 (Key Responsibility) | 范畴 (Scope) | 典型实现 (Typical Implementation) |
GoF 工厂方法 | 延迟实例化,提供扩展性 |
定义创建对象的接口,由子类决定实例化的具体类 2。 | 技术层面,关注单个产品的多态创建。 | 在父类中定义抽象的工厂方法,子类重写该方法。 |
GoF 抽象工厂 | 保证产品族的一致性 |
提供创建一系列相关或相互依赖对象的接口 4。 | 技术层面,关注多个相关产品的一致性。 | 定义一个包含多个工厂方法的接口,由具体工厂类实现整个产品族的创建。 |
GoF 构建器 (Builder) | 分步构建复杂对象 |
将一个复杂对象的构建与其表示分离,允许同样的构建过程创建不同的表示 25。 | 技术层面,关注具有多个可选部分或复杂构建步骤的对象。 | 链式调用 set 方法,最后调用 build 方法返回最终产品。 |
DDD 工厂 | 保证领域对象的初始完整性和有效性 |
原子性地创建完整的、满足所有不变量的聚合 21。封装复杂的创建逻辑和业务规则 22。 | 领域层面,关注业务语义的正确性和一致性边界。 |
可以是聚合根上的静态方法,或是独立的工厂服务类 17。 |
这张表格清晰地揭示了它们之间的本质区别。开发者在学习了GoF模式后,往往试图将其直接套用在DDD中,例如仅仅为了实现多态而使用工厂方法,却忽略了不变量的强制执行。通过并列比较,可以有效避免这种误解,帮助开发者为特定场景选择最合适的工具。
第四章:DDD工厂的实现模式
在实践中,DDD工厂主要有两种实现形式:聚合根上的静态工厂方法和独立的工厂服务。选择哪一种取决于聚合创建逻辑的复杂性及其外部依赖。
4.1 模式一:聚合根上的静态工厂方法
当聚合的创建逻辑相对简单,且不依赖于任何外部服务(如仓储或其他领域服务)时,最直接、最简洁的实现方式是在聚合根类上定义一个公共的静态工厂方法 17。
这种方法的优点是:
-
高内聚性:创建逻辑与它所创建的类紧密地放在一起,符合信息专家原则。
-
提升代码表达力:静态方法可以拥有具有业务含义的名称,这比通用的
new
关键字更能体现“统一语言(Ubiquitous Language)” 26。例如,Order.placeNewOrder(...)
和Order.createRepeatOrder(...)
清晰地表达了不同的业务场景。 -
强制正确的创建路径:通常将类的构造函数声明为
private
或internal
,强制所有客户端代码必须通过这些具名的静态工厂方法来创建实例,从而确保不变量在创建时得到执行 16。
Java代码示例:
Java
public class Order {
private OrderId id;
private CustomerId customerId;
private List<OrderLine> orderLines;
private Money totalAmount;
//... 其他属性
// 构造函数设为私有,防止外部直接调用new
private Order(OrderId id, CustomerId customerId, List<OrderLine> orderLines) {
this.id = id;
this.customerId = customerId;
this.orderLines = new ArrayList<>(orderLines);
// 在构造函数内部执行不变量计算
this.totalAmount = calculateTotal(orderLines);
}
// 公共静态工厂方法,作为创建新订单的唯一入口
public static Order placeOrder(CustomerId customerId, List<ProductItem> items) {
// 1. 前置条件检查(不变量)
if (items == null |
| items.isEmpty()) {
throw new IllegalArgumentException("Order must contain at least one item.");
}
// 2. 业务逻辑:将产品条目转换为订单行
List<OrderLine> lines = items.stream()
.map(item -> new OrderLine(item.getProductId(), item.getQuantity(), item.getPrice()))
.collect(Collectors.toList());
// 3. 生成唯一标识
OrderId orderId = OrderId.generate();
// 4. 调用私有构造函数创建实例
Order newOrder = new Order(orderId, customerId, lines);
// 5. 可以发布领域事件
// newOrder.registerEvent(new OrderPlacedEvent(newOrder.id, newOrder.customerId, newOrder.totalAmount));
return newOrder;
}
private static Money calculateTotal(List<OrderLine> lines) {
return lines.stream()
.map(OrderLine::getTotal)
.reduce(Money.ZERO, Money::add);
}
//... 其他业务方法
}
4.2 模式二:独立的工厂服务
当聚合的创建逻辑变得复杂,特别是当它需要依赖其他组件(如仓储、领域服务或外部API)来获取信息或执行验证时,将工厂逻辑抽取到一个独立的类中是更优的选择 24。
这种方法的优点是:
-
关注点分离:聚合根类可以专注于其生命周期内的业务行为,而工厂类则专门负责复杂的创建逻辑,遵循单一职责原则。
-
可测试性:独立的工厂类可以通过依赖注入(Dependency Injection)来接收其依赖项(如仓储的接口)。在单元测试中,可以轻松地使用模拟(Mock)对象替换这些依赖项,从而隔离地测试创建逻辑 28。
-
管理依赖:避免了将基础设施的依赖(如仓储)注入到领域模型(聚合根)中,保持了领域模型的纯粹性。
Java代码示例:
Java
// 独立的订单工厂服务
@Service // 或者其他表示组件的注解
public class OrderFactory {
private final CustomerRepository customerRepository;
private final PricingService pricingService;
// 通过构造函数注入依赖
@Autowired
public OrderFactory(CustomerRepository customerRepository, PricingService pricingService) {
this.customerRepository = customerRepository;
this.pricingService = pricingService;
}
public Order createPremiumOrder(CustomerId customerId, List<ProductItem> items) {
// 1. 依赖外部服务进行验证
Customer customer = customerRepository.findById(customerId)
.orElseThrow(() -> new CustomerNotFoundException(customerId));
if (!customer.isPremium() |
| customer.isBlacklisted()) {
throw new InvalidCustomerForPremiumOrderException("Customer is not eligible for premium orders.");
}
// 2. 复杂的业务规则检查
List<OrderLine> lines = createOrderLinesWithDynamicPricing(items, customer);
Money totalAmount = calculateTotal(lines);
// 3. 跨聚合的不变量检查
if (totalAmount.isGreaterThan(customer.getPremiumOrderLimit())) {
throw new OrderLimitExceededException("Premium order amount exceeds customer limit.");
}
// 4. 生成唯一标识并创建聚合
OrderId orderId = OrderId.generate();
// Order的构造函数现在可能是包级私有(package-private)或公共的,但由工厂来保证其正确创建
Order newOrder = new Order(orderId, customerId, lines);
return newOrder;
}
private List<OrderLine> createOrderLinesWithDynamicPricing(List<ProductItem> items, Customer customer) {
// 调用定价服务为高级客户获取特殊价格
return items.stream()
.map(item -> {
Money dynamicPrice = pricingService.calculatePriceFor(item.getProductId(), customer);
return new OrderLine(item.getProductId(), item.getQuantity(), dynamicPrice);
})
.collect(Collectors.toList());
}
//... 其他辅助方法
}
4.3 对比分析:如何选择正确的实现方式
选择哪种工厂实现模式并非一个教条式的决定,而应基于一个务实的、随需求演进而变化的决策过程。以下是一个可操作的决策框架:
-
从最简单的开始:对于一个新建的、创建逻辑简单的聚合,且其创建不依赖任何外部信息,始终从聚合根上的静态工厂方法开始 26。这是最简洁、内聚性最高的方式。
-
识别复杂性的引入:随着业务需求的发展,假设现在创建订单需要检查客户是否在黑名单中。这个“黑名单状态”属于
Customer
聚合。因此,订单的创建逻辑现在需要查询CustomerRepository
。 -
到达临界点:此时,你无法将
CustomerRepository
注入到一个静态方法中。静态方法与依赖注入框架天然不兼容,并且会引入对具体基础设施实现的硬编码依赖,破坏可测试性 29。这就是一个明确的信号,表明当前的实现方式已无法满足需求。 -
重构为独立工厂:将创建逻辑从聚合根的静态方法中移出,创建一个独立的
OrderFactory
类。将CustomerRepository
作为依赖,通过构造函数注入到这个新的工厂类中。原先的客户端代码现在调用OrderFactory
的实例方法来创建订单。
这个演进式的路径避免了过度设计。它不是要求开发者一开始就使用最复杂的模式,而是指导他们根据实际需求的增长,在恰当的时机进行重构。这体现了一种专家级的务实设计思想:架构决策是由不断演变的需求驱动的,而非僵化的规则。
第三部分:实战应用与高级主题
理论知识需要通过实践来巩固。本部分将通过一个完整的Java案例来展示DDD工厂的实际应用,并探讨它与其他关键DDD模式之间错综复杂的关系。
第五章:综合Java案例研究:“订单处理”限界上下文
我们将构建一个简化的订单处理场景,以此作为展示DDD工厂如何运作的实践平台。
5.1 定义Order
聚合
首先,我们定义构成Order
聚合的各个领域对象。这是一个真实的、非简化的聚合结构,包含了聚合根、实体和值对象。
-
值对象 (Value Objects):这些对象没有唯一标识,通过其属性值来定义。它们是不可变的。
Java// OrderId.java - 订单ID值对象 public final class OrderId { private final UUID value; // 构造函数、equals、hashCode、toString... } // CustomerId.java - 客户ID值对象 public final class CustomerId { private final Long value; //... } // Money.java - 金钱值对象,处理货币运算,避免浮点数精度问题 public final class Money { private final BigDecimal amount; private final Currency currency; // 构造函数、add、subtract、isGreaterThan等方法... public static final Money ZERO = new Money(BigDecimal.ZERO, Currency.getInstance("CNY")); } // ProductInfo.java - 描述一个被购买的产品信息的值对象 public final class ProductInfo { private final String productId; private final int quantity; //... }
-
实体 (Entity):
JavaOrderLine
(订单项)是一个实体,因为它在Order
聚合内部具有本地唯一标识(例如,通过产品ID区分),并且其状态(如数量)可能会改变。// OrderLine.java - 订单项实体 public class OrderLine { private String productId; private int quantity; private Money pricePerItem; private Money total; // 构造函数,计算总价 public OrderLine(String productId, int quantity, Money pricePerItem) { this.productId = productId; this.quantity = quantity; this.pricePerItem = pricePerItem; this.total = pricePerItem.multiply(quantity); } // Getters... }
-
聚合根 (Aggregate Root):
JavaOrder
是聚合的根,封装了所有订单项,并负责维护整个订单的一致性。// Order.java - 订单聚合根 public class Order { private OrderId id; private CustomerId customerId; private List<OrderLine> orderLines; private Money totalAmount; private Instant creationDate; private OrderStatus status; // 包级私有构造函数,只能由同一包内的Factory调用 Order(OrderId id, CustomerId customerId, List<OrderLine> orderLines) { this.id = id; this.customerId = customerId; this.orderLines = new ArrayList<>(orderLines); this.totalAmount = calculateTotal(orderLines); this.creationDate = Instant.now(); this.status = OrderStatus.CREATED; // 构造完成后立即检查不变量 validateInvariants(); } private void validateInvariants() { if (this.orderLines.isEmpty()) { throw new DomainException("Order must have at least one line."); } if (this.totalAmount.isLessThan(Money.ZERO)) { throw new DomainException("Order total amount cannot be negative."); } } //...其他业务方法如 cancel(), ship()等 }
5.2 实现独立的OrderFactory
现在,我们创建一个独立的OrderFactory
。它将依赖于CustomerRepository
和ProductRepository
来完成创建前复杂的业务验证。
Java
// 仓储接口定义(位于Domain层)
public interface CustomerRepository {
Optional<Customer> findById(CustomerId customerId);
}
public interface ProductRepository {
Optional<Product> findById(String productId);
}
// OrderFactory.java
@Component
public class OrderFactory {
private final CustomerRepository customerRepository;
private final ProductRepository productRepository;
@Autowired
public OrderFactory(CustomerRepository customerRepository, ProductRepository productRepository) {
this.customerRepository = customerRepository;
this.productRepository = productRepository;
}
//... createOrder 方法将在这里实现
}
5.3 代码走查:在工厂中强制执行不变量
createOrder
方法是工厂的核心。下面的代码将详细展示它如何一步步地编排复杂的创建逻辑,以确保最终产出的Order
聚合是完全有效的。
Java
// 在 OrderFactory.java 中
public Order createOrder(CustomerId customerId, List<ProductInfo> itemsToOrder) {
// 步骤 1: 基本输入验证
if (itemsToOrder == null |
| itemsToOrder.isEmpty()) {
throw new IllegalArgumentException("Cannot create an order with no items.");
}
// 步骤 2: 依赖仓储进行业务规则验证
// 验证客户是否存在且状态有效
Customer customer = customerRepository.findById(customerId)
.orElseThrow(() -> new DomainException("Customer with ID " + customerId + " not found."));
if (customer.isSuspended()) {
throw new DomainException("Cannot create order for a suspended customer.");
}
// 步骤 3: 循环处理订单项,并进行库存和价格检查
List<OrderLine> orderLines = new ArrayList<>();
for (ProductInfo itemInfo : itemsToOrder) {
Product product = productRepository.findById(itemInfo.getProductId())
.orElseThrow(() -> new DomainException("Product with ID " + itemInfo.getProductId() + " not found."));
// 检查库存是否充足
if (!product.hasSufficientStock(itemInfo.getQuantity())) {
throw new DomainException("Insufficient stock for product " + product.getName());
}
// 创建订单项,使用产品当前的价格
orderLines.add(new OrderLine(product.getId(), itemInfo.getQuantity(), product.getPrice()));
}
// 步骤 4: 执行一个更复杂的跨实体不变量检查
// 例如:新客户的第一笔订单金额不能超过 500 元
Money totalAmount = orderLines.stream().map(OrderLine::getTotal).reduce(Money.ZERO, Money::add);
if (customer.isNewCustomer() && totalAmount.isGreaterThan(new Money(new BigDecimal("500.00")))) {
throw new DomainException("New customer's first order cannot exceed 500 CNY.");
}
// 步骤 5: 所有检查通过,原子性地创建聚合
// 调用Order的包级私有构造函数,传递所有已验证和准备好的数据
OrderId newOrderId = OrderId.generate();
Order newOrder = new Order(newOrderId, customerId, orderLines);
// 步骤 6 (可选): 可以在这里或在应用服务层扣减库存
// product.decreaseStock(quantity);
// productRepository.save(product);
// 步骤 7: 返回一个完整、有效的聚合实例
return newOrder;
}
这个详细的代码走查过程,是许多开发者理解DDD工厂精髓的“顿悟时刻”。它清晰地展示了工厂如何作为协调者,在实例化对象之前,编排了一系列复杂的验证、查询和计算,最终兑现了其“创建完整有效聚合”的核心承诺。
第六章:工厂与仓储的精妙协作
在DDD中,工厂(Factory)和仓储(Repository)是另外一对容易引起混淆的概念。它们都与对象的生命周期有关,但关注的阶段截然不同。理清它们的关系是实现一个清晰、健壮的领域层的关键。
6.1 澄清职责:创建 vs. 重建
一个简洁而深刻的类比可以帮助我们区分它们的职责:
-
工厂(Factory) 负责一个聚合生命周期的开始。它的意图是“从无到有地创造一个新的领域概念” 26。例如,
orderFactory.createOrder(...)
。 -
仓储(Repository) 负责管理聚合生命周期的中间和结束。它的意图是“从持久化存储中找到一个已经存在的领域概念” 30。例如,
orderRepository.findById(...)
。
客户端调用工厂是因为它想创建一个新事物。客户端调用仓储是因为它想获取一个旧事物。这两个意图截然不同,因此它们的接口也应该反映这种差异。
6.2 模式:在仓储实现中使用工厂
当仓储从数据库或其他持久化介质中检索数据时(例如,获取到一个JPA实体或一个数据传输对象DTO),它需要将这些数据**重建(Reconstitute)**成一个富领域模型中的聚合对象。这个重建过程,本质上也是一种对象的“创建”,同样可能涉及到复杂的组装逻辑。
为了避免在仓储的实现中重复这套组装逻辑,一个非常强大且符合DRY(Don't Repeat Yourself)原则的模式是:仓储的实现委托给工厂来完成对象的重建任务 30。这样,所有关于如何将零散数据组装成一个有效
Order
聚合的知识,都集中存放在唯一的地方——OrderFactory
。
6.3 代码示例:JPAOrderRepository
利用OrderFactory
然而,这里存在一个设计上的细微之处。“创建新聚合”和“从持久化状态重建聚合”虽然都涉及到对象的组装,但它们的上下文和所需的不变量检查是不同的。
-
创建新聚合:需要执行所有业务规则验证,因为输入来自不可信的外部。
-
重建聚合:假设存储在数据库中的数据已经是过去某个时间点的有效状态。因此,重建时通常不需要再次执行那些耗时的业务验证(如检查客户状态或调用外部服务),只需将数据正确地映射到领域对象的属性上即可。
直接在OrderFactory
上增加一个reconstitute(...)
方法,并让JPAOrderRepository
调用它,可能会导致接口的职责不纯。应用服务关心createOrder
,而仓储实现关心reconstitute
。这违反了接口隔离原则(Interface Segregation Principle, ISP)。
一个更精良的设计是为这两个不同的客户端(应用服务和仓储)提供不同的接口:
-
定义两个职责单一的接口(在Domain层):
Java// INewOrderFactory.java - 给应用服务使用 public interface INewOrderFactory { Order createOrder(CustomerId customerId, List<ProductInfo> itemsToOrder); } // IOrderReconstitutionFactory.java - 给仓储实现使用 public interface IOrderReconstitutionFactory { Order reconstitute(OrderId id, CustomerId customerId, List<OrderLine> lines, OrderStatus status,...); }
-
让
JavaOrderFactory
实现这两个接口:// OrderFactory.java @Component public class OrderFactory implements INewOrderFactory, IOrderReconstitutionFactory { //... 依赖注入等 @Override public Order createOrder(CustomerId customerId, List<ProductInfo> itemsToOrder) { //... 如前文所示的完整创建逻辑 } @Override public Order reconstitute(OrderId id, CustomerId customerId, List<OrderLine> lines, OrderStatus status,...) { // 重建逻辑:通常是直接调用构造函数,信任传入的数据是有效的 // 这里可以有一个特殊的、参数更多的构造函数用于重建 return new Order(id, customerId, lines, status,...); } }
-
在仓储实现中使用重建接口:
Java// JPAOrderRepository.java (位于Infrastructure层) @Repository public class JPAOrderRepository implements OrderRepository { private final SpringDataJpaOrderRepo jpaRepo; private final IOrderReconstitutionFactory reconstitutionFactory; // 依赖于重建接口 @Autowired public JPAOrderRepository(SpringDataJpaOrderRepo jpaRepo, IOrderReconstitutionFactory reconstitutionFactory) { this.jpaRepo = jpaRepo; this.reconstitutionFactory = reconstitutionFactory; } @Override public Optional<Order> findById(OrderId orderId) { return jpaRepo.findById(orderId.getValue()) .map(this::mapToDomain); // 将JPA实体映射为领域对象 } private Order mapToDomain(OrderJpaEntity entity) { // 调用工厂的重建方法 return reconstitutionFactory.reconstitute( new OrderId(entity.getId()), new CustomerId(entity.getCustomerId()), //... 映射其他属性 ); } //... save() 方法则需要将领域对象映射回JPA实体 }
这种基于ISP的设计,清晰地分离了不同场景下的创建职责,使得依赖关系更精确,系统架构更清晰、更符合SOLID原则。它体现了从“能工作”到“做得好”的专家级设计演进。
第七章:DDD工厂与其他模式的关系
DDD工厂并非孤立存在,它与设计模式生态系统中的其他成员,如构建器(Builder)和领域服务(Domain Service),有着明确的分工与协作关系。
7.1 工厂 vs. 构建器:原子创建 vs. 分步构建
构建器模式(Builder Pattern)的核心在于分步构建一个复杂对象,它允许用户通过链式调用来设置对象的各个部分,最后通过一个build()
方法生成最终实例 25。这对于创建具有大量可选参数或配置项的对象(如一个复杂的查询对象或一个UI组件)非常有用 20。
然而,在DDD中,聚合的创建必须是原子的,并且必须确保在创建完成时满足所有不变量。分步构建的过程会让对象在build()
被调用前处于一个不完整或无效的中间状态,这与DDD的核心原则相悖。
因此,对于聚合的创建,工厂模式是首选。构建器模式可以作为工厂内部的一个实现细节。例如,OrderFactory
的createOrder
方法内部可以利用一个OrderBuilder
来辅助组装Order
对象,但对外部客户端而言,OrderFactory
的接口仍然是单一的、原子性的创建方法。
7.2 集成工厂与领域服务
领域服务(Domain Service)用于封装那些不适合放在任何实体或值对象上的领域逻辑,特别是那些需要协调多个聚合的操作 34。
在这种跨聚合的业务流程中,领域服务常常会与工厂协作。一个典型的场景是,一个领域服务消费一个或多个已有的聚合,然后利用工厂来创建一个新的聚合。
例如,一个电子商务系统可能有一个PromotionalCampaignService
(促销活动服务)。当一个“买一送一”的活动触发时,这个服务可能会接收一个原始的Order
聚合,然后调用OrderFactory
的某个特定方法(如createBonusOrderFor(originalOrder)
)来生成一个包含赠品的、金额为零的新Order
聚合。这清晰地展示了不同DDD构建块如何组合在一起,共同完成一个复杂的业务用例。
第四部分:最佳实践与总结
掌握DDD工厂不仅需要理解其理论和实现,更需要遵循一系列经过实践检验的最佳实践,以确保其在项目中发挥最大效用,同时避免常见的陷阱。
第八章:高效工厂设计的最佳实践
8.1 坚持单一职责原则(SRP)
工厂的设计是应用单一职责原则的绝佳范例。一个类应该只有一个变更的理由 35。
-
工厂的职责:封装创建聚合的复杂逻辑,保证聚合初始状态的有效性。其变更的理由是“创建规则的改变” 20。
-
聚合的职责:封装其生命周期内的业务行为和状态流转。其变更的理由是“业务流程的改变”。
将这两者分离,使得创建逻辑的修改不会影响到聚合的业务行为,反之亦然。这大大提高了代码的健壮性和可维护性 37。
8.2 设计原子且富有表达力的工厂方法
-
原子性:工厂方法必须是原子的。它应该接收所有必要的信息,完成所有验证和组装,然后返回一个完整的、立即可用的聚合实例 17。绝不能返回一个“半成品”,然后要求客户端继续完成初始化。
-
富有表达力:工厂方法的命名应遵循“统一语言”,清晰地反映其业务意图 20。避免使用泛泛的
create()
,而应使用如placeOrderFromShoppingCart()
、rescheduleAppointment()
或openNewAccount()
这样具有明确业务含义的名称。这使得代码本身就成为领域知识的载体,降低了沟通成本。
8.3 妥善处理依赖与外部服务
当工厂的创建逻辑需要外部信息时(例如,查询数据库以检查用户唯一性,或调用外部API获取汇率),必须使用独立的工厂服务,并通过依赖注入来管理这些依赖项 23。
-
依赖倒置:工厂应依赖于抽象(如
ICustomerRepository
接口),而不是具体实现。这使得工厂与基础设施层解耦,极大地增强了可测试性。 -
接口隔离:如前文所述,为不同的创建场景(如“全新创建”与“持久化重建”)提供不同的、职责更单一的接口,是一种更精良的设计。
8.4 反模式:何时应避免使用工厂
工厂模式虽好,但并非万能丹药。它本身也增加了代码的复杂性,如果被滥用,就会导致过度设计 34。
-
避免过度使用:对于简单的、没有复杂不变量或外部依赖的领域对象(特别是值对象),直接使用公共构造函数是完全可以接受的,并且是更简单、更直接的选择 17。
-
判断标准:引入工厂的驱动力应该是创建的复杂性。当你发现一个构造函数变得臃肿,或者创建逻辑开始渗透到应用服务层,或者创建过程需要访问基础设施时,这些都是引入工厂的强烈信号。在此之前,保持简单。
第九章:综合与最终建议
本次深度解析之旅,我们从对象创建的普遍性问题出发,深入到DDD对一致性和完整性的严格要求,最终聚焦于DDD工厂这一关键模式。我们探讨了它的核心职责、两种主流实现方式,并通过一个详尽的案例展示了其在真实世界中的应用,最后还阐明了它与其他模式的协作关系和设计最佳实践。
现在,我们可以得出一个核心结论:
DDD工厂远不止是一个创建型设计模式;它是领域完整性的守护者。它的根本使命是确保每一个聚合在进入领域世界的那一刻,就已经是一个完整的、有效的、严格遵守所有业务不变量的健壮个体。
对于正在实践DDD的开发者和架构师,最后的建议是务实的:
始终从最简单的实现开始。如果一个聚合的创建逻辑可以通过一个清晰的构造函数或一个静态工厂方法来表达,那就坚持使用它。只有当创建的复杂性——无论是源于内部逻辑的增长,还是外部依赖的引入——真正成为一个问题时,才将它重构为一个独立的、可注入依赖的工厂服务。让设计的演进由领域本身的复杂性来驱动,这正是领域驱动设计的精髓所在。