DDD理论学习系列——案例及目录


1.引言

在针对大型的复杂领域进行建模时,聚合、实体和值对象之间的依赖关系可能会变得十分复杂。在某个对象中为了确保其依赖对象的有效实例被创建,需要深入了解对象实例化逻辑,我们可能需要加载其他相关对象,且可能为了保持其他对象的领域不变性增加了额外的业务逻辑,这样即打破了领域的单一责任原则(SRP),又增加了领域的复杂性。

那如何去创建复杂的领域对象呢?因为复杂的领域对象的生命周期可能需要协调才能进行创建。 这个时候,我们就可以引入创建类模式——工厂模式来帮忙,将对象的使用与创建分开,将对象的创建逻辑明确地封装到工厂对象中去。

2. DDD中的工厂

我们有必要先理清工厂和工厂模式。

DDD中工厂的主要目标是隐藏对象的复杂创建逻辑;次要目标就是要清楚的表达对象实例化的意图。

而工厂模式是计模式中的创建类模式之一。借助工厂模式我们可以很好实现DDD中领域对象的创建。

而针对工厂模式的实现主要有四种方式:

  • 简单工厂:简单实用,但违反开放封闭;
  • 工厂方法:开放封闭,单一产品;
  • 抽象工厂:开放封闭,多个产品;
  • 反射工厂:可以最大限度的解耦。

具体实现可以参考创建相似对象,就交给『工厂模式』吧

3.封装内部结构

当需要为聚合添加元素时,我们不能暴露聚合的结构。我们以添加商品到购物车为例,来讲解如何一步一步的使用工厂模式。

一般来说,添加到购物车需要几个步骤:

  1. 加载用户购物车
  2. 获取商品税率
  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。返回的WishListItemWishList聚合根的实体。另外一点我们之所以在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.总结

对象创建不是一个领域的关注点,但它确实存在于应用程序的领域层中。通过使用工厂可以有效的保证领域模型的干净整洁,以确保领域模型的对现实的准确表达。使用工厂具有以下好处:

  1. 工厂将领域对象的使用和创建分离。
  2. 通过使用工厂类,可以隐藏创建复杂领域对象的业务逻辑。
  3. 工厂类可以根据调用者的需要,创建相应的领域对象。
  4. 工厂方法可以封装聚合的内部状态。

然而,并不是任何需要实例化对象的地方都要使用工厂。只有当用工厂比使用构造函数更有表现力时,或存在多个构造函数容易造成混淆时,或者对要创建对象所依赖的对象不关心时,才选用工厂进行对象的创建。

参考资料:

《Patterns, Principles, and Practices of Domain-Driven Design》

DDD理论学习系列(11)-- 工厂的更多相关文章

  1. DDD理论学习系列——案例及目录

    目录 DDD理论学习系列(1)-- 通用语言 DDD理论学习系列(2)-- 领域 DDD理论学习系列(3)-- 限界上下文 DDD理论学习系列(4)-- 领域模型 DDD理论学习系列(5)-- 统一建 ...

  2. DDD理论学习系列(12)-- 仓储

    DDD理论学习系列--案例及目录 1. 引言 DDD中Repository这个单词,主要有两种翻译:资源库和仓储,本文取仓储之译. 说到仓储,我们肯定就想到了仓库,仓库一般用来存放货物,而仓库一般由仓 ...

  3. DDD理论学习系列(13)-- 模块

    DDD理论学习系列--案例及目录 1. 引言 Module,即模块,是指提供特定功能的相对独立的单元.提到模块,你肯定就会想到模块化设计思想,也就是功能的分解和组合.对于简单问题,可以直接构建单一模块 ...

  4. DDD理论学习系列(2)-- 领域

    DDD理论学习系列目录 1. 引言 领域一词,主要有以下两个意思: 一国主权所达之地. 学术思想或社会活动的范围. 不管是指国家的主权范围也好还是学术活动范围,都是在讲一个范围,一个界限. 比如我们常 ...

  5. DDD理论学习系列(4)-- 领域模型

    DDD理论学习系列目录 1.引言 我们还是先来拆词理解,领域模型可以拆为"领域"和"模型"二词. 领域:按照我们之前的文章的理解,DDD中的领域是指软件系统要解 ...

  6. DDD理论学习系列(5)-- 统一建模语言

    DDD理论学习系列--案例及目录 1.引言 上一节讲解了领域模型,领域模型主要是将业务中涉及到的概念以面向对象的思想进行抽象,抽象出实体对象,确定实体所对应的方法和属性,以及实体之间的关系.然后将这些 ...

  7. DDD理论学习系列(6)-- 实体

    DDD理论学习系列--案例及目录 1.引言 实体对应的英语单词为Entity.提到实体,你可能立马就想到了代码中定义的实体类.在使用一些ORM框架时,比如Entity Framework,实体作为直接 ...

  8. DDD理论学习系列(7)-- 值对象

    DDD理论学习系列--案例及目录 1.引言 提到值对象,我们可能立马就想到值类型和引用类型.而在C#中,值类型的代表是strut和enum,引用类型的代表是class.interface.delega ...

  9. DDD理论学习系列(8)-- 应用服务&领域服务

    DDD理论学习系列--案例及目录 1. 引言 单从字面理解,不管是领域服务还是应用服务,都是服务.而什么是服务?从SOA到微服务,它们所描述的服务都是一个宽泛的概念,我们可以理解为服务是行为的抽象.从 ...

随机推荐

  1. [python] 1、python鼠标点击、移动事件应用——写一个自动下载百度音乐的程序

    1.问题描述: 最近百度总爱做一些破坏用户信任度的事——文库金币变券.网盘限速,吓得我赶紧想办法把存在百度云音乐中的歌曲下载到本地. http://yinyueyun.baidu.com/ 可问题是云 ...

  2. Java常用类之【日期相关类】

    一.日期类 Java语言提供了2个类来处理日期 Date类 Date类以毫秒来表示特定的日期 构造方法 Date date = new Date(); System.out.println(date) ...

  3. 纯css实现横向下拉菜单

    自己最近在捣腾css代码,不光是js能做很多有趣的东西,这货也可以做很多东西,现在把自己的一些作品分享给大家. 做得有点粗糙,大家根据这个思路来,可以自己修改修改. 关于demo:这是一个横向下拉的菜 ...

  4. 阻止Nmap的黑手

    大大们办网站,首先要做的就是安全,一般黑客都会用nmap扫描我们的网站这是我们所不希望看到的一下我提供几个过滤机制,nmap是无法扫描到你的 1 #iptables -F 2 #iptables -A ...

  5. CentOS上安装redis记录

    下载稳定版 curl -O http://download.redis.io/releases/redis-stable.tar.gz tar -zxvf redis-stable.tar.gz cd ...

  6. Java下一个简单的数据库分库帮助类

    简介    前面两篇文章主要讲了数据库读写分离和分表分库的一些问题,这篇文章主要讲一下我个人实现的一个分表分库项目.     在此之前,我有写过一个.Net的分库,最近在做Java的项目,就顺便做出一 ...

  7. 原生ajax异步请求基础知识

    一.同步交互与异步交互的概念: * 同步交互:客户端向服务器端发送请求,到服务器端进行响应,这个过程中,用户不能做任何其他事情(只能等待响应完才能继续其他请求). * 异步交互:客户端向服务器端发送请 ...

  8. 前端基于JQgrid实现自定义列头展示

    先上效果图   因为公司项目的需要,并且公司只有我这一个能写js的前端,这个自定义展示jqgrid列选项的需求依然是交由我写,辣么就分享一下我的工作成果. //初始化函数 multiSelectCol ...

  9. AngularJS高级程序设计读书笔记 -- 模块篇

    一. 模块基础 1. 创建模块 <!DOCTYPE html> <html ng-app="exampleApp"> <head> <ti ...

  10. 360安全检测出的WordPress漏洞的修复方法

    1.跨站脚本攻击(XSS) 这个漏洞注意是因为用户评论可以提交代码,有安全风险.虽然你的WordPress以及是最新版,但是你的WordPress主题却不一定跟着更新!因此,需要稍微修改一下评论相关的 ...