领域模型对象的主力军是实体与值对象。这些实体与值对象又被聚合统一管理起来,形成一个个具有一致生命周期的“命运共同体”自治单元。管理领域模型对象的生命周期,实则就是管理聚合的生命周期。
所谓“生命周期”,就是聚合对象从创建开始,在成长过程中经历各种状态的变化,直至最终消亡的过程。在软件系统中,生命周期经历的各种状态取决于存储介质,分为两个层次:内存与硬盘,分别对应对象的实例化与数据的持久化。
当今的主流开发语言大都具备垃圾回收的功能。因此,除了少量聚合对象可能因为持有外部资源(通常要避免这种情形)而需要手动释放内存资源,在内存层次的生命周期管理,主要牵涉到的工作就是创建。一旦创建了聚合的实例,聚合内部各个实体与值对象的状态变更就都发生在内存中,直到聚合对象因为没有引用而被垃圾回收。
由于计算机没法做到永不宕机,且内存资源相对昂贵,一旦创建好的聚合对象在一段时间用不上,就需要被持久化到外部存储设备中,以避免其丢失,节约内存资源。无论采用什么样的存储格式与介质,在持久化层次,针对聚合对象的生命周期管理不外乎增、删、改、查这4个操作。
从对象的角度看,生命周期代表了一个实例从创建到回收的过程,就像从出生到死亡的生命过程。而数据记录呢?生命周期的起点是指插入一条新记录,该记录被删除就是生命周期的终点。领域模型对象的生命周期将对象与数据记录二者结合起来,换言之就是将内存(堆与栈)管理的对象与数据库(持久化)管理的数据记录结合起来,用二者共同表达聚合的整体生命周期。
在领域模型的设计要素中,由聚合根实体的构造函数或者工厂负责聚合的创建,而后对应数据记录的“增删改查”则由资源库进行管理。
聚合在工厂创建时诞生;为避免内存中的对象丢失,由资源库通过新增操作完成聚合的持久化;若要修改聚合的状态,需通过资源库执行查询,对查询结果进行重建获得聚合;在内存中进行状态变更,然后通过持久化确保聚合对象与数据记录的一致;直到删除了持久化的数据,聚合才真正宣告死亡。
以文章聚合的生命周期为例:
// 创建文章
// 通过Post的工厂方法在内存中创建
Post post = Post.of(title, author, abstract, content);
//持久化到数据库
postRepository.add(post);
// 发布文章
// 根据postId查找数据库的Post,在内存重建Post对象
Post post = postRepository.postOf(postId);
// 内存的操作,内部会改变文章的状态
post.publish();
//将改变的状态持久化到数据库
postRepository.update(post);
// 删除文章
//从数据库中删除指定文章
postRepository.remove(postId);
一、工厂概述
1.工厂与构造函数
许多面向对象语言都支持类通过构造函数创建它自己。
对象自己创建自己,就好像自己扯着自己的头发离开地球表面,完全不合情理,只是开发人员已经习以为常了。然而,构造函数差劲的表达能力与脆弱的封装能力,在面对复杂的构造逻辑时,显得有些力不从心。
遵循“最小知识法则”,我们不能让调用者了解太多创建的逻辑,以免加重其负担,并带来创建代码的四处泛滥,何况创建逻辑在未来很有可能发生变化。基于以上因素考虑,有必要对创建逻辑进行封装。领域驱动设计引入工厂(factory)承担这一职责。
2.工厂与设计模式
工厂是创建产品对象的一种隐喻。《设计模式:可复用面向对象软件的基础》的创建型模式引入了工厂方法(factory method)模式、抽象工厂(abstract factory)模式和构建者(builder)模式,可在封装创建逻辑、保证创建逻辑可扩展的基础上实现产品对象的创建。
除此之外,通过定义静态工厂方法创建产品对象的简单工厂模式也因其简单性得到了广泛使用。
领域驱动设计的工厂并不限于使用哪一种设计模式。一个类或者方法只要封装了聚合对象的创建逻辑,都可以被认为是工厂。
2.工厂的职责
将创建复杂对象和聚合的职责分配给一个单独的对象,该对象本身并不承担领域模型中的职责,但是依然是领域设计的一部分。
工厂应该提供一个创建对象的接口,该接口封装了所有创建对象的复杂操作过程,同时,它并不需要客户去引用那个实际被创建的对象。
除了创建对象之外,工厂并不需要承担领域模型中的其他职责。
对于聚合来说,我们应该一次性地创建整个聚合,并且确保它的不变条件得到满足。
二、工厂的优点
在DDD中,工厂是生产领域模型的地方,特别是聚合。它为用户抽象了对象的创建过程,通过制定专门的语义(通用语言)和更精细地控制对象的实例化过程来达到这一目的。简而言之,工厂模式的主要目的是生产对象的实例并提供给调用者。
工厂的优点如下:
1.解耦:分离领域职责与创建工序
工厂的主要目的是分离模型的领域职责及其复杂的创建工序。模型创建本身就是一个复杂的操作,尤其在面对大而丰富的领域和关系众多的聚合时更是如此。工厂和聚合是天生的好搭档,因为聚合不仅要初始化各类数据,还要体现某种装配规则。把这个任务交给用户,显然超出了他们的意愿与能力范围。
模型本身并不适合承担装配自己的复杂操作,如果将其领域职责与创建逻辑混在一起,则会破坏领域模型的纯洁性。
同时,创建模型的职责也不适合放到应用层中。虽然应用层领域模型的用户将使用领域模型来实现用例和完成需求,但装配方式在某种程度上仍是一种领域逻辑,应用层代表了“业务”,但绝不代表“业务逻辑”。
所以,创建复杂对象是领域层的职责,但同时又不适合放在领域模型内部。因此,我们需要一个单独的领域对象来负责创建模型,不承担其他领域逻辑,这个对象就是工厂。
第一种实现方式:应用层创建对象。
public class CartService {
public void add(Product product , Guid cartId){
cart = cartRepository.of(cartId);
rate = TaxRateService.obtainTaxRateFor(product,country.id);
item = new CartItem(rate,product,id,product.price);
cart.add(item);
}
}
以上是一个跨境电商的购物车对象,添加一个购物项,必须计算相应税率。我们可以看到,应用层必须理解这个逻辑才能够创建购物项,这样领域逻辑就泄露到了应用层。下面是改进后的设计。
第二种实现方式:非工厂领域模型创建对象。
public class CartService {
public void add(Product product, Guid cartId){
cart = cartRepository.of(cartId);
cart.add(product);
}
}
//领域层
public class Cart {
public void add(Product product){
rate = TaxRateService.obtainTaxRateFor(product,country.id);
item = new CartItem(rate,product,id,product.price);
items.add(item);
}
}
这一版使用领域对象Cart来创建购物项,将领域逻辑保留在领域层。然而,购物车对象Cart和税费计算服务TaxRateService之间不可避免地产生了耦合,这破坏了Cart的纯洁性,未来任何与购物车内在逻辑无关的变化都会影响到购物车。这是自找麻烦,因此我们用工厂模式将两者解耦是最佳选择。
第三种实现方式:工厂创建对象。
public class CartService {
public void add(Product product, Guid cartId){
cart = cartRepository.of(cartId);
cart.add(product);
}
}
//领域层
public class Cart {
public void add(Product product){
items.add(CartItemFactory.createCartItemFor(product));
}
}
//工厂
public class CartItemFactory {
public static CartItem createCartItemFor(Product product,Country country){
rate = TaxRateService.obtainTaxRateFor(product,country.id);
item = new CartItem(rate,product,id,product.price);
return item;
}
}
工厂隐藏了创建对象的细节,从而购物车无须再关心与其内在业务逻辑无关的创建信息。
2.通用语言:让创建过程体现业务含义
工厂可以让模型更好地表达通用语言。为什么会这样呢?我们换个思路就明白了,工厂创建实例时,命名方法不一定是GetInstanceOf×××而可以根据通用语言来命名,如BookTicket(预订车票)、ScheduleMeeting(安排会议)、RegisterUser(注册用户)、Offer-Invitation(发送邀请)、ScheduleCalendarEntry(添加日程)。这些操作本质上都是要创建新对象的工厂方法,并且表现力要强得多。
public class Customer{
public Ticket bookTicket(TicketInfo ticketInfo){
Ticket aTicket = new Ticket(this.id,ticketInfo);
DomainPublisher.instance.publish(new TicketBookedEvent);
return aTicket;
}
}
BookTicket是客户对象中的一个工厂方法,用于返回一个车票的实例。我们把创建车票的工厂方法放在了Customer对象中,这与前面的不应将创建方法放入对象的说法并不矛盾,因为Customer是聚合根,在聚合根中放置创建成员的工厂是合适的。并非所有的工厂都是单独的领域服务,只要没产生多余的耦合,就可以灵活选择厂址。
事实上,很多工厂方法都不叫工厂,它们将创建新对象的操作与领域的通用语言相结合,使DDD的模型和代码都更具表现力。
3.验证:确保所创建的聚合处于正确状态
在工厂中创建新对象时,可以添加逻辑验证以确保创建出的聚合符合领域内在规则。
例如,在聚合根账户上创建订单,要满足账户必须有足够的余额。
public class Account {
public Order createOrder() {
//如果余额充足
if(hasEnoughMoney){