强化你的域:聚合的构建【翻译】

我们的应用复杂性已经达到它的临界点,于是我们决定把以前的贫血领域模型转成丰富的行为性的模型。但是什么是贫血模型?让我们看看Folower的6年前的定义:

一个贫血领域模型的基本特征是乍一看它挺像一个真实的对象。在一个领域里一些对象是用名词命名的,这些对象通过与真实领域模型相同的丰富的关系和结构来建立连接。当你观察行为的时候问题就来了,你会意识到这些对象几乎没有任何行为,它们仅仅比一袋访问器和设置器多一点点东西。事实上这些模型常常是根据一种设计规则创建的—不能把任何领域逻辑放到域对象中。取而代之的是有一些能够处理所有领域逻辑的服务对象。这些服务对象在域模型之上活动并且使用域模型作为数据。

对于CRUD应用,这些”域服务“数量应该很少,但是随着域服务数量开始增长,这对我们来说应该就是一个信号,我们需要已领域驱动设计的方式获取更丰富的行为。基于DDD思想创建一个应用和基于模型架构很不一样。基于模型架构,我们是从数据库表结构或者ERDs开始的,然后构建对象去匹配。基于DDD,我们是从交互和行为开始的,然后构建模型去匹配。但是我们遇到的第一个问题是,我们在第一个地方如何创建实体?我们的第一个单元测试需要创建一个实体,它应该从哪里来?

创建有效聚合体

有效性校验可能是应用中一个棘手的难题,因为我们常常看到有效性在使用数据时很少用,而是在使用命令时用的多。举个例子,一个人可能在屏幕上有一个”出生日期“的必填字段,但是我们有一个需求是遗留以及被导入的用户没有出生日期。所以很明显出生日期的要求取决于谁在创建这个人。

但是除了有效性之外就是实体的不变量。不变量是一个实体是实体的本质。我们问我们的客户,在我们的系统中没有出生日期的人是人吗?是的,有时是。没有姓名呢?不,在我们的系统中一个人必须有一些标示身份的特征,它们结合在一起定义了这个”人“。一笔订单需要一个订单号和一位客户。如果业务人员拿到一个没有客户信息的订单表单,他们会扔出去!注意这个不是有效性,但是其它一些完全是。我们现在要问了,一笔订单是一笔订单意味着什么呢?那就是不变量。

假设我们有一个相当简单的一组逻辑。如果发票省份和客户的省份是一样的话,我们会标明我们的订单是”本地“。非常简单的方法:

public class Order

{

          public bool IsLocal()

          {

               return Customer.Province==BillingProvince

           }

}

我简单的询问客户的省份与订单的发票省份。但是现在我进入一个相当奇怪的场景。

[Test]

public void Should_be_a_local_customer_when_provinces_are_equal()

{

          var order=new Order

          {

                BillingProvince=“Ontario”

           };

          var customer=new Customer

          {

                 Province=“Ontario”

           };

            var isLocal=order.IsLocal();

            isLocal.ShouldBeTrue();

}

没有正常的成功或失败的断言,我得到一个空引用异常!我忘记在订单对象上设置一位客户。

但是等一下—我怎么能创建一个没有客户的订单呢?一个没有客户的订单不是一个订单,因为我们的领域专家是这样对我们解释的。我们可以稍微走一条荒唐的路,加一个空引用检查。

但是等一下—这个永远不会在产线发生。我应该仅仅修复我们的测试代码然后继续,对吗?是的,如果你是构建一个基于CRUD系统(90%情况)的交易脚本的话我是同意的。虽然是这样,但是如果我们使用DDD,我们想让聚合根满足这个要求,也就是说聚合根的不变量必须满足所有操作。创建一个聚合根是一个操作,而且在代码中”new”是一个操作。现在,不变量肯定被满足。

让我们修改一个我们的订单类:

public class Order

{

          public Order(Customer customer)

          {

                Customer=customer;

           }

}

我们添加一个构造器,因此当创建订单时,所有的不变量都是满足的。我们的测试现在要修改。

[TestFixture]

public class Invariants

{

     [Test]

      public void Should_be_a_local_customer_when_provinces_are_equal()

      {

                var customer=new Customer

                {

                      Province=“Ontario”

                 };

                 var order=new Order(customer)

                  {

                          BillingProvince=“Ontario”

                   };

                   var isLocal=order.IsLocal();

                   isLocal.shouldBeTrue();

}

现在我们的测试通过了!

总结和其它选择

从数据驱动的方式转到这种方式,将会有一些痛苦。如果我们首先写一些持久层的测试,我们会进入“为什么我们测试订单持久化得时候需要一位客户?“,或者,为什么测试订单的所有逻辑时需要一位客户?当你开始基于一个没有强制它自己的不变量的领域模型写代码时,这种问题是不是很明显。如果不变量仅仅通过域服务来满足,对于理解在任何时候一个真正”订单“是什么的问题时非常的棘手。我们写代码的时候应该一直假设客户存在吗?我应该仅仅在使用它的时候再写吗?

如果我们的实体一直满足它的所有不变量因为它的设计不允许不变量被违背,那么违背的不变量就永远不会发生。我们不再需要考虑缺失客户的可能性,并且能够在这种强制的规则下构建我们的软件。实践中,我发现这种方式真的需要较少的代码,因为我们不允许进入那种需要思考的荒谬的场景。

但是通过构成器创建实体并不是唯一的方法。我们也有:

构造器模式

创建方法

通过已存在的聚合根

底线是—如果我们的实体需要特定信息才能被认为是一个实体时,不能创建一个不变量不被满足的实体。

相关文章:

实体内还是外有效性验证

使用传递管道处理横截关注点

更好的领域事件模式

使用Entity Framework scorecard领域建模

内部与外部事件比较


原文链接:Strengthening your domain:Aggregate Construction

作者:Jimmy Bogard

推荐阅读更多精彩内容