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》
DDD理论学习系列(11)-- 工厂的更多相关文章
- DDD理论学习系列——案例及目录
目录 DDD理论学习系列(1)-- 通用语言 DDD理论学习系列(2)-- 领域 DDD理论学习系列(3)-- 限界上下文 DDD理论学习系列(4)-- 领域模型 DDD理论学习系列(5)-- 统一建 ...
- DDD理论学习系列(12)-- 仓储
DDD理论学习系列--案例及目录 1. 引言 DDD中Repository这个单词,主要有两种翻译:资源库和仓储,本文取仓储之译. 说到仓储,我们肯定就想到了仓库,仓库一般用来存放货物,而仓库一般由仓 ...
- DDD理论学习系列(13)-- 模块
DDD理论学习系列--案例及目录 1. 引言 Module,即模块,是指提供特定功能的相对独立的单元.提到模块,你肯定就会想到模块化设计思想,也就是功能的分解和组合.对于简单问题,可以直接构建单一模块 ...
- DDD理论学习系列(2)-- 领域
DDD理论学习系列目录 1. 引言 领域一词,主要有以下两个意思: 一国主权所达之地. 学术思想或社会活动的范围. 不管是指国家的主权范围也好还是学术活动范围,都是在讲一个范围,一个界限. 比如我们常 ...
- DDD理论学习系列(4)-- 领域模型
DDD理论学习系列目录 1.引言 我们还是先来拆词理解,领域模型可以拆为"领域"和"模型"二词. 领域:按照我们之前的文章的理解,DDD中的领域是指软件系统要解 ...
- DDD理论学习系列(5)-- 统一建模语言
DDD理论学习系列--案例及目录 1.引言 上一节讲解了领域模型,领域模型主要是将业务中涉及到的概念以面向对象的思想进行抽象,抽象出实体对象,确定实体所对应的方法和属性,以及实体之间的关系.然后将这些 ...
- DDD理论学习系列(6)-- 实体
DDD理论学习系列--案例及目录 1.引言 实体对应的英语单词为Entity.提到实体,你可能立马就想到了代码中定义的实体类.在使用一些ORM框架时,比如Entity Framework,实体作为直接 ...
- DDD理论学习系列(7)-- 值对象
DDD理论学习系列--案例及目录 1.引言 提到值对象,我们可能立马就想到值类型和引用类型.而在C#中,值类型的代表是strut和enum,引用类型的代表是class.interface.delega ...
- DDD理论学习系列(8)-- 应用服务&领域服务
DDD理论学习系列--案例及目录 1. 引言 单从字面理解,不管是领域服务还是应用服务,都是服务.而什么是服务?从SOA到微服务,它们所描述的服务都是一个宽泛的概念,我们可以理解为服务是行为的抽象.从 ...
随机推荐
- JVM类加载以及执行的实战
前几篇文章主要是去理解JVM类加载的原理和应用,这一回讲一个可以自己动手的例子,希望能从头到尾的理解类加载以及执行的整个过程. 这个例子是从周志明的著作<深入理解Java虚拟机>第9章里抄 ...
- sql还原(.bak文件还原)
第一步: 右键“数据库”,选择“还原数据库” 第二步: 选择“设备”,然后选择“…” 第三步: 添加备份文件(这里使用MyDB.bak) 第四步: 勾选“还原”复选框,进度显示“已完成” 第五步: 最 ...
- Asp.Net Core写个共享磁盘文件Web查看器
本篇和大家分享的是一个磁盘文件查看系统,严格来说是使用NetCore写的一个Web系统应用,由于NetCore跨平台特性,我生成了exe的运行包,只需要配置运行电脑ip+端口,即可在浏览器中通过IP+ ...
- 【小练习04】HTML+CSS--医药健康小页面
要求实现如下效果图: 代码演示 <!DOCTYPE html> <html> <head> <meta charset="UTF-8"&g ...
- Java IO流之文件流
一.文件流分类 二.FileInputStream 三.FileOutputStream 四.FileReader 五.FileWriter 六.文件流应用 1,复制或剪切文件 2,读取文件信息 应用 ...
- cpp(第二章)
1. 函数参数空着,代表void 2. 换行符 endl(确保程序继续运行前刷新输出,将其立即显示在屏幕上)|| '\n' (不能保证,这意味着有些系统中,有时可能输入信息后才会出现) 3.小谈cou ...
- javaCV开发详解之技术杂烩:javaCV能帮我们做什么?能实现什么功能?ffmpeg和openCV能实现功能,javaCV如何做到更快、更简单的实现相应的功能?等等一堆实用话题
前言: 该篇文章旨在帮助刚接触javaCV的盆友系统的认识音视频.javaCV.图像处理相关的体系知识和一些实用的知识. 序: javaCV早期因为内置了openCV库,所以常用来做图像识别应用,现在 ...
- [原创]嵌入CEF遇到的问题及解决方案
这几天程序嵌入谷歌浏览器,各种坑,不容易,记录之...希望到此为止 1. 开了多进程模式之后,渲染进程RenderProcess断点没有进入. 只有在单进程模式(CefSingleProcess为tr ...
- hibernate操作步骤(代码部分)
1.加载hibernate的核心配置文件 2.创建SessionFactory对象 3.使用SessionFactory创建Session对象 4.开启事务(手动开启) 5.写具体逻辑crud,增删改 ...
- Python之道1-环境搭建与pycharm的配置django安装及MySQL数据库配置
近期做那个python的开发,今天就来简单的写一下开发路线的安装及配置, 开发路线 Python3.6.1+Pycharm5.0.6+Django1.11+MySQL5.7.18 1-安装Python ...