DDD理论学习系列(11)-- 工厂

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理论学习系列(1)-- 通用语言

  • DDD领域驱动之干货 (一)

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

  • DDD理论学习系列(3)-- 限界上下文

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

  • 事件总线知多少(2)

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

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

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

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

  • DDD理论学习系列(9)-- 领域事件

  • DDD理论学习系列(10)-- 聚合

  • 从事件和DDD入手来构建微服务

  • DDD领域驱动之干货 (一)

  • WeText项目:一个基于.NET实现的DDD、CQRS与微服务架构的演示案例

  • 【DDD/CQRS/微服务架构案例】在Ubuntu 14.04.4 LTS中运行WeText项目的服务端

  • 基于.NET CORE微服务框架 -surging的介绍和简单示例 (开源)

  • 剥析surging的架构思想

  • 基于.NET CORE微服务框架 -谈谈surging的服务容错降级

  • 我眼中的ASP.NET Core之微服务

  • .NET Core 事件总线,分布式事务解决方案:CAP

原文地址:http://www.cnblogs.com/sheng-jie/p/7215812.html


.NET社区新闻,深度好文,微信中搜索dotNET跨平台或扫描二维码关注

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/324336.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

ssl2647-线段树练习4【线段树】

正题 题意 一条长m线&#xff0c;有n条长度不同的线段&#xff0c;查询x到x1有多少条线 解题思路 标记直接覆盖颜色数&#xff0c;然后找到那个点&#xff0c;之后向上到根节点把所有叠加的线统计 代码 #include<cstdio> #include<cstring> using namespace s…

记得完成寒假作业~

今天&#xff0c;对于学生们来说&#xff0c;没有比这在开心的日子了&#xff0c;因为&#xff0c;他们放假了&#xff01;&#xff01;&#xff01;有的同学&#xff0c;周一来了就开始盼望着周五&#xff0c;每天都扳着手指头数还剩几天才能到周五&#xff0c;周一心里就想&a…

数据库的最简单实现

转载自 数据库的最简单实现 所有应用软件之中&#xff0c;数据库可能是最复杂的。MySQL的手册有3000多页&#xff0c;PostgreSQL的手册有2000多页&#xff0c;Oracle的手册更是比它们相加还要厚。 但是&#xff0c;自己写一个最简单的数据库&#xff0c;做起来并不难。Reddi…

Raft协议安全性保证

分布式系统中主要的问题就是如何保持节点状态的一致性&#xff0c;不论发生任何failure&#xff0c;只要集群中大部分的节点可以正常工作&#xff0c;则这些节点具有相同的状态&#xff0c;保持一致&#xff0c;在client看来相当于一台机器。 一致性问题本质就是replicated sta…

2021,春节联欢会

今天,3班和4班都组织了春节联欢会,每个节目都是有同学们精心准备的,非常非常的精彩。 上午,搬着电脑去上课的时候,发现黑板上已经被红红的彩带占上了,估计学生们这时候的心理就是,应该不能上课了吧。然后我就在“众目睽睽”下拆下来,将投影仪幕布放下来继续上课,哈哈哈…

网关过滤

内置网关过滤 自定义过滤

你,的寒假作业写多少了?

大家好&#xff0c;我是雄雄&#xff0c;欢迎关注公众号【雄雄的小课堂】。一晃寒假已过10多天&#xff0c;亲爱的同学们&#xff0c;你的寒假作业写多少了&#xff1f;是一点都没动呢还是多少动了点了&#xff1f;昨天晚上在3班的群里发了几套课程&#xff0c;刚发就有几位同学…

MassTransitamp;amp;Sagas分布式服务开发ppt分享

saga&#xff0c;与分布式相关&#xff0c;最早被定义在Hector Garcia-Molina和Kenneth Salem的论文"Sagas"中。这篇论文提出了一个saga机制来作为分布式事务的替代品以解决长时间运行的分布式事务&#xff08;long-running process&#xff09;的问题。这篇论文认为…

全局过滤器

自定义全局过滤器

最长回文子串(Longest Palindromic Substring)

转载自 最长回文子串&#xff08;Longest Palindromic Substring&#xff09;——三种时间复杂度的解法 子串&#xff1a;小于等于原字符串长度由原字符串中任意个连续字符组成的子序列 回文&#xff1a;关于中间字符对称的文法&#xff0c;即“aba”(单核)、“cabbac”(双核)…

小白入门级的视频剪辑软件

大家好&#xff0c;我是雄雄&#xff0c;欢迎关注公众号【雄雄的小课堂】。前言昨天&#xff0c;给班级群里面分享了个视频剪辑软件——剪映PC端&#xff0c;并交代同学们&#xff0c;剪辑视频可以以它来入手&#xff0c;我大致的看了看&#xff0c;PC端的剪映功能基本上能满足…

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

1. 引言 DDD中Repository这个单词&#xff0c;主要有两种翻译&#xff1a;资源库和仓储&#xff0c;本文取仓储之译。 说到仓储&#xff0c;我们肯定就想到了仓库&#xff0c;仓库一般用来存放货物&#xff0c;而仓库一般由仓库管理员来管理。当工厂生产了一批货物时&#xf…

Windows 通过 SecureCRT 8.x 上传文件到Linux服务器

转载自 Windows 通过 SecureCRT 8.x 上传文件到Linux服务器 1、SecureCRT 连接 Linux 服务器&#xff0c;这一步操作简单&#xff1a; 2、连接并登录成功后&#xff0c;直接在连接成功的页签上 右键 -> Connect SFTP Session 打开SFTP窗口&#xff1a; 3、在新的SFTP页签…

没有回家的等于没过年

大家好&#xff0c;我是雄雄&#xff0c;欢迎关注公众号【雄雄的小课堂】。今天是农历2021年正月初二&#xff0c;新的一年&#xff0c;祝大家牛年大吉&#xff0c;身体健康&#xff0c;万事如意&#xff01;今年由于各方面原因并未回家过年&#xff0c;感觉没回家等于没过年&a…

.NET Core引入性能分析引导优化

“性能分析引导优化&#xff08;Profile Guided Optimization&#xff0c;缩写PGO&#xff09;”是一项原生编译技术&#xff0c;可用于生成高度优化的代码。它通过一个两步编译过程实现优化——用第一步记录相关执行信息&#xff0c;然后第二步用那些信息构建一个改进过的二进…

手把手教你搭建Maven项目

大家好&#xff0c;我是雄雄&#xff0c;欢迎关注微信公众号【雄雄的小课堂】。今天给大家分享的是“手把手教你买基金”&#xff0c;奥&#xff01;&#xff01;不对&#xff0c;不好意思&#xff0c;是“手把手教你使用idea搭建Maven项目”&#xff01;前言大家都知道&#x…