领域驱动设计之对象如何创建

板桥banq 原创书籍《复杂软件设计之道:领域驱动设计全面解析与实战》

目录

  1. 领域的定义
  2. 如何编写类?
  3. 对象如何创建
  4. SQL语句要不要写

第三章

  前两章我们讲述了如何从领域中发现模型,主要从实体 逻辑规则和逻辑流程三个方面去探寻,这三者的有机灵活不同组合是体现一个领域区别于其他领域的主要地方。

  领域模型发现后,进入详细设计阶段,是使用数据表还是是OO语言的类型概念实现,取决于你对哪种工具掌握的熟练程度,当然语言类型比较能接近领域模型概念,模型的落地降落过程没有经过翻译转换,不容易跑偏。

  两种落地方式不同,决定了你的模型对象是贫血还是充血,如果是以数据表为主,那么模型成为贫血,只有属性,没有业务行为,关键问题是,这些贫血模型会有各种很相似但是又有点不同的类,阅读或管理起来不方便,创建时如果有多个构造函数,更让人感觉复杂,这些号称VO或DTO的贫血对象是架构各个层都要使用的,这实际上将复杂性泄露到前端或其他地方,使得系统变得人为地复杂。

对象的唯一标识

  为什么一个对象会有多个构造函数,也就是说,这个对象可以各种方式构建,这实际上违背单一职责原则,如何破解?关键没有为该对象找到唯一标识,也就是能够唯一标识这个对象与其他对象区别的地方,比如

```
public class A{
  int activityId;
  int taskId;

}

```
  这个A类中有两个Id, 分别是活动Id和任务Id, 那么这个A到底是活动类还是任务类呢?或者是两者结合的类?那么活动和任务结合的东东是什么?我们日常语词中有这个概念吗?还是生造的概念呢?对于这些凭空捏造的概念都要警惕,其实它可能是代表一种关系类,代表活动和任务的关系,但是在OO中我们表达关系不是这样显式,而是很巧妙的。
```
public class Task{
  Activity activity;
}
```
  将活动对象变成任务的属性,这种关系称组合依赖,如果关系更紧密,称聚合依赖,如果熟悉IOC或Spring,这里可以实现依赖注射。

  这就是OO语言表达关系的隐性方式,把两个对象像积木块组合一起,就代表他们之间存在关系,所以,关系不像A类那样专门用两个Id组合起来表达,那是一种数据表的表达方式。

  这样的关系类很显然没有唯一标识,因为不代表现实世界中任何实体事物,如果有很多个这样对象,我们不用去区分他们,这些对象其实就是值对象,各种数据值组合在一起的对象,如前面提到的A。

  而有唯一标识的实体对象,则会要去区分,这个这里逻辑反过来也是,因为要区分,所以需要唯一标识,否则怎么区分呢?比如两个人站在你面前,你能够根据面部特征作为唯一标识区分他们,据说小小孩还没有这个能力,爸爸如果是双胞胎,轮流抱婴儿,婴儿是无法区分的,因为他没有培养发现唯一标识的能力,现在机器学习能够迅速面部识别,通说明抓住了唯一标识。

在我们的软件信息系统里标识唯一很简单,只要确定这个事物是个实体,分配一个唯一Id数值就可以。注意这段话,关键是你要能确定这个事物是实体,如何确定?抓唯一标识啊,唯一特征,几种属性组合成唯一特征,比如名称 年龄 住址 和性别这些组合起来,我们能够立即识别可以表达'人'这样的类别概念,当然如果在其他上下文,也可以表达'宠物';如果只是名称和住址组合在一起,有可能是表达的是'地址'概念。

所以,一个实体的取名其实也是唯一标识的表达,不只是其内容包含一个抽象的Id数值,而且包含了让它区别于其他概念的基本属性,没有这些基本属性,它就什么也不是,没有边界,没有名称,没有Id。有时我在想,为什么西方人对个体标识如此敏感和尖锐?而我们从面向对象的普及程度看,对个体对象的边界和标识竟然要通过类似我写的这些OO理论知识去普及,而不是如同识别人脸一样变成一种本能和习惯,这其中原因还是令人深思的。

以上讲了两个DDD概念,值对象和实体,有唯一标识的是实体,否则就是值对象,DDD通过这样的区分如同将颜色分为黑白两种,这种基本分类其实很有好处,能够让我们在以后代码实现和运行中分别对待,就像上帝将人分男女,以后领域对象逃不过这两种,复杂的是他们之间如何组合。

对象的创建

  只有抓住了唯一标识,才能用这唯一标识去构造这个对象,如同知道了一个人名字,才能用名字把他找出来,当然,身份证号是更准确的唯一标识。

```
public class Person{
  int ID;
  public Person(int ID){
  this.ID = ID:
}

}
```
我们创建这个类的对象:
```
Person person = new Person(123456);
```
需要首先有ID数值,才能创建这个person对象。这样才能让很多个person对象之间有区分。

前面我们说过,这个ID只是唯一标识的抽象表达,真正决定这个person对象区别于其他对象的是有两种情况:

1. 类型区别,person对象属于Person类,该类型区别于其他非Person类型的是基本属性不同,它由名称 年龄 住址 和性别这些组合而成。

2. 数值区别。person对象中名称 年龄 住址 和性别这些属性的数值不同,最终区别是这些属性相加,如同1+2+3+4=10。最终区别是10这个值不同。

那么唯一标识的代表ID=类型区别+数值区别。当一个对象以ID为入参被创建时,它同时兼具了这两个区别。

如果一个对象只以ID为入参被创建,其实只是具备了类型特征,还没有赋值,这样的对象只能是个半成品,并不能使用,只有变成成品,才能放心使用,对象的创建细节才不会泄露到使用环节,做到职责分离,这里有两个职责,创建和使用,不会把两个职责混合在一起,符合单一职责。

很显然,熟悉GOF设计模式的人明白这里需要使用工厂模式,把对象的创建放在工厂里实现:
```
public class PersonFactory{

  public Person create(int ID){

    Person person = new Person(123456);
    person.setName('xx');
    person.setSex('male');
....

}

}
```
这个创建应该是一个事务过程,要么把对象的所有属性创建赋值,变成一个成品出厂,要么就无法出厂,不能是半成品出厂。

当然,如果语言支持即用即赋值,我们可以将基本属性赋值完成后,其他字段属性的值可在使用时赋值,比如使用Hibernate的懒赋值功能也可以。

对象应该只有一个构造函数

前面我们分析了使用工厂模式生产完整成品,成品生产是使用一个有ID的构造函数,那么能不能有两个构造函数呢?

```
public class Person{
  int ID;
  public Person(int ID){
    this.ID = ID:
  }

  public Person(){

  }
}
```
这里有两个构造函数,一个是有参数的,一个是没有参数的,前面我们分析了有参数id的意义,因为id代表实体的唯一标识,实际等价于整个实体对象,而如果使用没有参数的构造函数,那就代表这个对象创建时没有标识,不只是半成品,简直就是残品,只有类型属性,没有任何赋值,无法代表任何一个指定的实体对象,那你创建它有什么意义呢?
当然,有时序列化框架可能需要这样无参数的构造函数,他用来接收远程的数据转换,所以说充血对象在分布式环境里面确实使用起来不是很方便,对于这种情况,我们可以专门构造一个值对象用来进行序列化。

如果还有其他各种构造函数,如下:
```
public class Person{
  int ID;
  public Person(int ID){
    this.ID = ID:
  }

  public Person(String name){
    .......
  }
}
```
这里的各种构造函数的入参是名称,虽然名称也是这个对象的唯一标识的一部分,但是只是一部分,它没有用id更能全局性的代表这个对象。
ID=名称+性别+出生地址+其他基本属性。
当然,如果还有其他类型的构造函数,都没有我们使用aid作为构造函数准确,所以一个对象只能有一个构造函数。

总结


我们讨论了领域模型中的实体对象必须有唯一标识,而且通过这个为标识作为句柄,也就是构造函数的入参,从而推理出一个实体对象,只能有一个构造函数,多一个构造函数只能使得实体的复杂性增加,而且会将这种复杂些扩展到实体对象的使用过程中。

下一章