DDD理论学习系列(11)-- 工厂
1.引言
在针对大型的复杂领域进行建模时,聚合、实体和值对象之间的依赖关系可能会变得十分复杂。在某个对象中为了确保其依赖对象的有效实例被创建,需要深入了解对象实例化逻辑,我们可能需要加载其他相关对象,且可能为了保持其他对象的领域不变性增加了额外的业务逻辑,这样即打破了领域的单一责任原则(SRP),又增加了领域的复杂性。
那如何去创建复杂的领域对象呢?因为复杂的领域对象的生命周期可能需要协调才能进行创建。 这个时候,我们就可以引入创建类模式——工厂模式来帮忙,将对象的使用与创建分开,将对象的创建逻辑明确地封装到工厂对象中去。
2. DDD中的工厂
我们有必要先理清工厂和工厂模式。
DDD中工厂的主要目标是隐藏对象的复杂创建逻辑;次要目标就是要清楚的表达对象实例化的意图。
而工厂模式是计模式中的创建类模式之一。借助工厂模式我们可以很好实现DDD中领域对象的创建。
而针对工厂模式的实现主要有四种方式:
- 简单工厂:简单实用,但违反开放封闭;
- 工厂方法:开放封闭,单一产品;
- 抽象工厂:开放封闭,多个产品;
- 反射工厂:可以最大限度的解耦。
具体实现可以参考创建相似对象,就交给『工厂模式』吧。
3.封装内部结构
当需要为聚合添加元素时,我们不能暴露聚合的结构。我们以添加商品到购物车为例,来讲解如何一步一步的使用工厂模式。
一般来说,添加到购物车需要几个步骤:
- 加载用户购物车
- 获取商品税率
- 创建新的购物车子项
相关的应用层代码如下:
namespace Application { public class AddProductToBasket { // ...... public void Add (Product product, Guid basketId) { var basket = _basketRepository.FindBy (basketId); var rate = TaxRateService.ObtainTaxRateFor (product.Id, country.Id); var item = new BasketItem (rate, product.Id, product.price); basket.Add (item); // ... } } }
在以上代码中,应用服务需要了解如何创建BasketItem
(购物车子项)的详细逻辑。而这不应该时应用服务的职责,应用服务的职责在于协调。我们尝试做以下改变来避免暴露聚合的内部结构。
namespace Application { public class AddProductToBasket { // ...... public void Add (Product product, Guid basketId) { var basket = _basketRepository.FindBy (basketId); basket.Add (product); // ... } } } namespace DomainModel { public class Basket { // ...... public void Add (Product product) { if (Contains (product)) GetItemFor (product).IncreaseItemQuantitBy (1); else { var rate = TaxRateService.ObtainTaxRateFor (product.Id, country.Id); var item = new BasketItem (rate, product.Id, product.price); _items.Add (item); } } } }
以上代码展示了Basket
(购物车)对象提供一个Add
方法,用来完成添加商品到购物车的业务逻辑,对应用服务隐藏了购物车如何存储商品的细节。另外购物车聚合能够确保其内部集合的完整性,因为它可以确保领域的不变性。通过这种方式,完成了职责的切换,现在的应用服务要简单的多。
然而,却引入了一个新的问题。为了根据商品创建有效的购物车子项,购物车需要提供一个有效的税率。为了创建这个税率,它要依赖一个TaxRateService
(税率服务)。获取创建购物车子项依赖的税率,这并不属于购物车的职责。而按照上面的实现,购物车承担了第二责任,因为它必须始终了解如何创建有效的购物车子项以及在哪里去获取有效的税率。
为了避免购物车承担额外的职责和隐藏购物车子项的内部结构。下面我们引入一个工厂对象来封装购物车子项的创建,包括获取正确的税率。
namespace DomainModel { public class Basket { // ...... public void Add (Product product) { if (Contains (product)) GetItemFor (product).IncreaseItemQuantitBy (1); else _items.Add (BasketItemFactory.CreateItemFor (product, deliveryAddress)); } } public class BasketItemFactory { public static void CreateBasketFrom (Product product, Country country) { var rate = TaxRateService.ObtainTaxRateFor (product.Id, country.Id); return new BasketItem (rate, product.Id, product.price); } } }
引入工厂模式后,购物车的职责单一了,且隔离了来自购物车子项的变化,比如当税率变化时,或购物车子项需要其他信息创建时,都不会影响到购物车的相关逻辑。
4.隐藏创建逻辑
考虑这样的需求:订单创建成功后,进行发货处理时,要求根据订单的商品和收件人信息选择合适的快递方式。比如默认发顺丰,顺丰无法送达的选择中国邮政。
根据这个需求,我们可以抽象出一个Kuaidi
(快递)对象用来封装快递信息,和一个Delivery
(发货)对象用来封装发货信息(货物、收件人信息、快递等)。创建Delivery
的职责我们可以放到Order
中去,但针对Order
来说它并不知道要创建(选择)哪一种Kuaidi
(快递)。所以,我们可以创建一个KuaidiFactory
工厂负责Kuaidi
对象的创建。
namespace DomainModel { public class Order { // ... public Delivery CreateFor (IEnumerable<Item> items, destination) { var kuaidi = KuaidiFactory.GetKuaidiFor (items, destination.Country); var delivery = new Delivery (items, destination, kuaidi); SetAsDispatched (items, delivery); return delivery; } } public class KuaidiFactory { public static Kuaidi GetKuaidiFor (IEnumerable<Item> deliveryItems, DeliveryAddress destination) { if (Shunfeng.CanDeliver (deliveryItems, destination)) { return new Shunfeng (deliveryItems, destination); } else { return new EMS (deliveryItems, destination); } } } }
如上代码所示,工厂类中我们封装了快递的选择逻辑。
当要创建的对象类型有多个选择,且客户端并不关心创建类型的选择时,我们可以在领域层使用工厂中去定义逻辑去决定要创建对象的类型。
5.聚合中的工厂方法
提到工厂,并不是都需要需要创建独立的工厂类来负责对象的创建。一个工厂方法也可以存在于一个聚合中。
比如这样一项需求,顾客可以将购物车中的商品移到愿望清单中去。
第一,这个动作是发生在购物车上的,所以我们可以毫不犹豫的在购物车中定义该行为。第二,将商品添加到愿望清单中去,就需要创建一个愿望清单子项。
namespace DomainModel { public class Basket { // ..... public WishListItem MoveToWishList (Product product) { //首先检查购物车中是否包含此商品 if (BasketContainsAnItemFor (product)) { //从购物车中获取该商品对应的子项 var basketItem = GetItemFor (product); //调用工厂方法根据购物车子项创建愿望清单子项 var wishListItem = WishListItemFactory.CreateFrom (basketItem); //从购物车中移除购物车子项 RemoveItemFor (basketItem); return wishListItem; } } } }
从上面可以看出Basket
暴露一个方法用于将BasketItem
转换为WishListItem
。返回的WishListItem
是WishList
聚合根的实体。另外一点我们之所以在Basket
中调用工厂去创建WishListItem
对象,是因为Basket
包含了创建愿望清单子项所需的全部信息。在创建了WishListItem
之后,对于Basket
对象来说它的任务就完成了。
6.使用工厂重建对象
在项目中,如果没有借助ORM进行数据模型与领域模型之间的映射,或者通过Web服务从一个老旧系统中获取领域对象,都需要我们对领域对象进行重建以满足领域的不变性。使用工厂来重建领域对象相对来说要比直接创建要复杂。
考虑这样的场景:顾客可以在已购订单中点击再次购买按钮,所有订单项全部重新添加到购物车中去。
这个场景就属于购物车对象的重建,跟直接创建购物车对象就不同了。因为将订单中的所有子项恢复到购物车中去,我们就需要额外确保领域的不变性。比如订单子项对应的商品现在是否下架,如果下架我们是直接抛出异常,还是仍旧创建一个锁定的购物车子项,标记其为已下架状态?
namespace DomainModel { public class Order { // ...... public Basket AddToCartFromOrder (Guid id) { OrderDTO rawData = ExternalService.ObtainOrder (id.ToString ()); var basket = BasketFactory.ReconstituteBasketFrom (rawData); return basket; } } namespace DomainModel { public class BasketFactory { // ... public static Basket ReconstituteBasketFrom (OrderDTO rawData) { Basket basket; // ... foreach (var orderItem in rawData.Items) { //是否下架 if (!ProductServie.IsOffTheShelf (orderItem.ProductId)) { var newBasketItem = newBasketItem (orderItem.ProductId, orderItem.Qty); basket.Add (newBasketItem); } else { throw new Exception ("订单中该商品已下架,无法重新购买!"); } } // ..... return basket; } } }
7.总结
对象创建不是一个领域的关注点,但它确实存在于应用程序的领域层中。通过使用工厂可以有效的保证领域模型的干净整洁,以确保领域模型的对现实的准确表达。使用工厂具有以下好处:
- 工厂将领域对象的使用和创建分离。
- 通过使用工厂类,可以隐藏创建复杂领域对象的业务逻辑。
- 工厂类可以根据调用者的需要,创建相应的领域对象。
- 工厂方法可以封装聚合的内部状态。
然而,并不是任何需要实例化对象的地方都要使用工厂。只有当用工厂比使用构造函数更有表现力时,或存在多个构造函数容易造成混淆时,或者对要创建对象所依赖的对象不关心时,才选用工厂进行对象的创建。
参考资料:
《Patterns, Principles, and Practices of Domain-Driven Design》