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到微服务,它们所描述的服务都是一个宽泛的概念,我们可以理解为服务是行为的抽象.从 ...
随机推荐
- java面试题之int和Integer的区别
int和Integer的区别 1.Integer是int的包装类,int则是java的一种基本数据类型 2.Integer变量必须实例化后才能使用,而int变量不需要 3.Integer实际是对象的引 ...
- vs2015未安装 Style 的 Visual Studio 语言支持
解决方案:在浏览器搜索下载安装Microsoft ASP.NET and Web Tools即可 下载地址:https://www.microsoft.com/en-us/download/confi ...
- phpcms通过URL传参
在PHPCMS中都会遇到通过URL传参数的问题,但是默认的只能取到$catid.$page等这类的值,特别是伪静态之后,想获得其他参数根本不可能,有的人用$_GET["参数"]这种 ...
- python之numpy库[2]
python-numpy csv文件的写入和存取 写入csv文件 CSV (Comma‐Separated Value, 逗号分隔值),是一种常见的文件格式,用来存储批量数据. 写入csv文件 np. ...
- 关于QT5使用QtScript解析QJsonArray数组的问题
首先得在pro文件中加入QT+=script 然后导入相应的头文件 include <QStringList> #include <QtScript/QScriptEngine> ...
- maven插件打包可执行jar
pom文件加 <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</grou ...
- 解决(防止)DDOS攻击的另一种思想
本方案适合作最后的处理方案. 在服务器遭到DDOS攻击后,防火墙.高防盾或者其他的方案都已经失去了效力,这时运维人员无任何方案可以处理,并且只能任由DDOS攻击或关闭服务器时,该方案可以有限的抵挡大部 ...
- Redis数据类型之Set
前言:set类似于数学上面的集合概念,包含的元素无序,不能重复,能进行交.并.差操作. 一.内部原理 set数据结构,也是随着元素数目的多少而变化.当set中添加 ...
- 关于MATLAB处理大数据坐标文件201762
经过头脑风暴法想出了很多特征,目前经过筛选已经提交了两次数据,数据提交结果不尽如人意,但是收获很大. 接下来继续提取特征,特征数达到27时筛选出20条特征,并找出最佳搭配
- Java学习笔记--动态代理
动态代理 1.JDK动态代理 JDK1.3之后,Java提供了动态代理的技术,允许开发者在运行期创建接口的代理实例.JDK的动态代理主要涉及到java.lang.reflect包中的两个类:Proxy ...