实现领域驱动设计(DDD)中对实体的设计及使用

作者: admin 分类: 领域驱动设计 发布时间: 2019-11-27 18:36  阅读: 41 views

实体是我们在开发中经常用的类对象。当我们需要考虑一个对象的个性特征,或者需要区分不同的对象时,就引入了实体这个领域概念。一个实体是一个唯一的东西,并且可以在相当长的一段时间内持续地变化。也可以对实体做多次修改,故一个实体对象可能和它先前的状态大不相同。但它们拥有相同的身份标识(唯一id等),它们依然是同一个实体。

唯一的身份标识和可变性特征将实体对象(ENTITY)和值对象区分开来。一个基于CURD(增删改查)的系统是从数据层面出发,并不是一个好的业务模型。

第一部分:唯一标识的创建

在设计实体时,首先要考虑实体的本质特征,特别是实体的唯一标识和对实体的查找,而不是一开始便关注实体的属性和行为。实体的唯一标识并不见得一定有助于对实体的查找和匹配。将唯一标识用户实体匹配通常取决于标识的可读性。(如,CRM系统中的根据人名查找功能,由于存在大量重名,所以姓名不可能是唯一标识;财务软件中的功能可以根据税号查找,因为政府为每个公司分配了唯一的税号)

值对象可以用户存放实体的唯一标识,值对象不变,可以保证实体身份的稳定性,并且与身份标识相关的行为也可以得到集中处理。这样就可以避免将身份标识相关的行为泄露到模型的其他部分或者客户端中。

以下是一些常用的创建实体身份标识的策略,从简单到复杂:

1. 用户提供一个或多个初始唯一值作为程序输入,程序应该保证这些初始值是唯一的。
2. 程序内部通过某种算法自动生成唯一标识,此时可以使用一些类库或框架,当然程序自身也可以完成这样的功能(UUID,GUID等)。
3. 程序依赖于持久化存储,比如数据库,来生成唯一标识。
4. 另一个限界上下文(组件\程序)已经生成了唯一标识,这作为程序的输入,用户可以从中选择。

以上每种技术方案都存在副作用,其中之一便是将关系型数据库用于对象持久化的时候,并将行为泄漏到了领域模型中。

  1. 用户提供唯一标识

复杂性和不可控性是需要用户自己生成高质量的标识。会产生重复、错误的风险。通常是将一些用户的输入作为实体的属性,但不作为唯一身份标识。(如论坛发帖,用户可以自行定义发帖的标题)

  1. 应用程序生成唯一标识

需要注意的是,如果应用程序处于集群环境或者分布在不同的计算节点中,要注意可能出现的重复性。有些方法可以生成完全唯一的标识,如UUID(Universally Unique Identifier)或者GUID(Globally Unique Identifier)。

以下是生成唯一标识的一种方法,每部生成的结果都会添加到最终的文本标识中

1. 计算节点的当前时间,以毫秒记
2. 计算节点的IP地址
3. 虚拟机(JAVA)中工厂对象实例的对象标识
4. 虚拟机(JAVA)中由同一个随机数生成器生成的随机数

以上可以产生一个128位的唯一值。通常该唯一值通过一个32字节或36自己的16进制数的字符串标识。JAVA中,以上方法被标准的UUID生成器所代替。如下:

//这是采用高度加密的伪随机数生成器,该生成器基于java.security.SecureRandom生成器。
String rawId = java.util.UUID.randomUUID().toString();

//这是采用对名字加码的方法,使用了java.security.MessageDigest类。
String rawId = java.util.UUID.nameUUIDFromBytes("Some text".getBytes()).toString();

//对生成的伪随机数进行加密
SecureRandom randomGenerator = SecureRandom();
int randomNumber = randomGenerator.nextInt();
String randomDigits = new Integer(randomNumber).toString();
MessageDigest encryptor = MessageDigest.getInstance("SHA-1");
byte[] rawIdBytes = encryptor.digest(randomDigits.getBytes());

//TODO 将rawIdBytes转换成16进制数的字符串表示。

注:采用以上做法时,可以将UUID生成器缓存起来。如果UUID实例由于服务器重启而丢失,重新创建后并不会对系统造成影响。

对于如此大的唯一标识,有时从内存使用的角度来看可能并不实际。通常来说我们并不会在用户界面上显示UUID,如“ f35ab21c-57dc-5274-c723-1de2f3z5e93a”的。

  1. 持久化机制生成唯一标识

向数据库获取一个序列值(Sequence)或递增值,结果总是唯一的。根据标识的所需范围,数据库可以生成2字节、4字节和8字节的唯一标识。JAVA中,2字节整数可以标识32767种不同的标识值;4字节整数可以表示 2147483647中标识..

缺点是性能上会有点慢,因为有连接数据库的损耗。一种解决方式是将数据库序列缓存在内存中,但是如果服务器节点需要重启,将会丢失很大一部分标识值区间。

4.另一个限界上下文提供唯一标识

如果另一个限界上下文用于给实体标识赋值,那么我们需要对每一个标识进行查找、匹配和赋值,其中最重要的是精确匹配。不同限界上下文的数据变更需要做同步操作,不仅要考虑本地的改变还要考虑外部系统的变化,略复杂

5.标识的生成时间

实体唯一标识的生成既可以在对象创建的时候,也可以发生在持久化对象的时候。所以,根据需求的不同,有时我们需要及早地生成实体标识,而有时标识生成时间则不那么重要。如下两种方式

延时生成标识
在Product初始化完成之后,系统将产生一个领域事件,该时间将保存在事件存储中。最后,所存储的时间将被外部限界上下文中的订阅方所接收【当并发插入数据时,会产生严重的bug】

及早生成标识
先向ProductRepository获取下一个实体标识,然后将该实体标识作为参数传递给Product的构造函数。

6.委派标识

部分ORM工具,通过自己的方式来处理对象的身份标识。如Hibernate使用数据库提供的机制,用一个数值序列来生成实体标识,如果我们自己的领域需要另外一种实体标识,就会产生冲突。这是我们可以使用两种标识,一种为领域所用,一种为ORM使用。这就是委派标识。

设计上可以使用层超类型,如下IdentifiedDomainObject便是层超类型。是个抽象基类,通过protected关键字,向客户端隐藏了委派主键。领域标识不需要作为数据库的主键,将委派标识作为主键。

public abstract class IdentifiedDomainObject implements Serializable{

    private long id = -1;

    public IdentifiedDomainObject(){
        super();
    }

    protected long id(){
        return this.id;
    }

    protected void setId(long anId){
        this.id = anId;
    }
}

注:程序一般不允许对已有的领域标识进行修改,否则将抛出异常。

第二部分:发现实体及其本质特征

在通用语言的术语中,名词用于给概念命名,形容词用于描述概念,而动词标识可以完成的操作。通用语言应该直接反应在代码中,如果想要保持设计文档的实时更新是非常困难的。

例如以下软件需求的变更:【粗浅的理解和深入的理解,对设计的结果完全不同】

version1.0

1.User存在于某个Tenant之下,并受该Tenant控制
2.必须对系统中的User进行认证
3.User可以处理自己的个人信息,包括名字和联系方式等
4.User的个人信息可以被其本人和Manager修改
5.User的安全密码是可以修改的

version2.0(累加)

Tenant可以邀请多个User进行注册
Tenant可以处于激活状态或失活状态
系统必须对User进行认证,并且只有当Tenant处于激活状态时才能对User进行认证

version3.0(累加,领域服务术语的使用)

租户(Tenant):一个有名字的企业订阅方,它提供身份和访问服务,同时还包括其他的在线服务。租户向用户发出注册邀请,并处理用户注册过程。
用户(User):一个租户下的注册用户.包含有个人名字和联系信息。一个用户拥有唯一的用户名和密码。
加密服务:对密码或其他敏感信息进行加密。

version4.0(累加)

激活租户:通过该操作激活一个租户,激活后再对租户的当前状态进行确认。
禁用租户:通过该操作禁用一个租户,在禁用一个租户时,用户可能还没有被认证。
认证服务:协调对用户的认证过程,首先需要保证他们所属的租户处于激活状态

version5.0(累加)

个人:包含并管理用户的个人信息,包括名字和联系方式等。

version6.0(累加)

向一个客户添加订单
使客户成为优先客户

需求的不断深入挖掘,会对实体创建有很大影响。代码示例说明参考《实现领域驱动设计》169~184的说明。

第三部分:验证实体属性(自封装)

自封装(从对象内部访问数据)来验证属性。
首先它为对象的示例变量和类变量提供了一层抽象,其次我们可以方便地在对象中访问其所引用对象的属性。还使得验证变得简单。如下:

public final class EmailAddress{

    private String address;

    public EmailAddress(String anAddress){
        super();
        this.setAddress(anAddress);
    }

    //自封装,在类内部验证参数
    private void setAddress(String anAddress){

        if(anAddress == null){
            throw new IllegalArgumentException("");
        }

        if(anAddress.length() == 0){
            throw new IllegalArgumentException("");
        }

        if(anAddress.length() > 100){
            throw new IllegalArgumentException("");
        }
        if(!java.util.regex.Pattern.matches("\\xx")){
            throw new IllegalArgumentException("");
        }
        this.address = anAddress;

    }

}

以上的EmailAddress类不是一个实体,而是值对象。作为了一个类的属性。这种方式验证,结合实际情况有点不适用,例如我们经常使用mybatis生成表相关对象。如果按照这种方式,可能每次生成时都要调整。还是结合实际情况做处理。

第四部分:跟踪变化

如果关心发生在模型中的一些重要事件,此时我们便应该对实体的一些特殊变化进行跟踪了。
跟踪变化最实用的方法是领域事件和事件存储。为领域专家所关心的所有状态改变都创造单独的事件类型,事件的名称和属性表明发生了什么样的事件。当命令操作执行完之后,系统发出这些领域事件。事件的订阅方可以接收发生在模型上的所有事件。接收事件后,订阅方将事件保存在事件存储中。 看之前的整理 事件源架构模式


   原创文章,转载请标明本文链接: 实现领域驱动设计(DDD)中对实体的设计及使用

如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!

发表评论

电子邮件地址不会被公开。 必填项已用*标注

更多阅读