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

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》

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,242评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,769评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,484评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,133评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,007评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,080评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,496评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,190评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,464评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,549评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,330评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,205评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,567评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,889评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,160评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,475评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,650评论 2 335

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,494评论 18 139
  • DDD理论学习系列——案例及目录 1.引言 聚合,最初是UML类图中的概念,表示一种强的关联关系,是一种整体与部分...
    圣杰阅读 12,403评论 39 27
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,047评论 25 707
  • 隔雾探花花愈艳,透云赏月月更浓。 化尽寒冰须日暖,扫荡残霾赖劲风。 燕啾唧,雀昵哝。陌上芳草又枯荣。 恨春来迟春不...
    汉水布衣阅读 512评论 1 5
  • 水彩是我没有接触过的领域,之前几次的大胆尝试基本都以失败告终。所以,我决定以归零的心态开始重新学起。找了一个公众号...
    板牙兔阅读 395评论 9 16