《实现领域驱动设计》读书笔记摘抄(5)—— 实体

Administrator 642 2022-03-19

第5章 实体

本章学习路线图

  • 当对具有“唯一性”的事物进行建模时,为什么需要考虑使用实体
  • 学习如何生成实体的唯一标识
  • 学习如何从实体设计中捕获通用语言
  • 学习如何表达实体的角色和职责
  • 学习如何对实体进行验证和持久化

首先考虑的是数据的属性(对应数据库的列)和关联关系(外键关联)而不是富有行为的领域概念,会导致表示领域模型的实体包含大量的getter和setter方法,虽然不是什么大错,但这却不是DDD的做法。

为什么使用实体

实体是一个唯一的东西,并且可以在相当长的一段时间内持续的变化,但由于他们拥有相同的身份标识,它们依然是一个实体。唯一的身份标识和可变性特征将实体对象和值对象(Value Object)区分开来。

有时实体不见得是一种适当的建模工具,更适合建模成值对象。

我们通过标识对对象进行区分,而不是属性,此时我们应该将标识作为主要的模型定义。同时我们需要保持简单的类定义,并且关注对象在其生命周期中的连续性和唯一性标识。我们不应该通过对象的状态形式和历史来区分不同的实体对象,对于什么时相同的东西,模型应该给出定义。

唯一标识

在设计实体时,我们首先需要考虑实体的本质特征,特别是实体对唯一标识和对实体的查找,而不是一开始便关注实体的属性和行为。只有在对实体的本质特征有用的情况下,才加入相应的属性和行为。将唯一标识用于实体匹配通常取决于标识的可读性。

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

  • 用户提供一个或多个初始唯一值作为程序输入,程序应该保证这些初始值是唯一的。
  • 程序内部通过某种算法自动生成身份标识,此时可以使用一些类库或者框架,当然程序自身也可以完成这样的功能。
  • 程序依赖于持久化存储,比如数据库,来沈城·生成唯一标识。
  • 另一个限界上下文(系统或程序)已经决定出了唯一标识,这作为程序的输入,用户可以在一组标识中进行选择。

用户提供唯一标识

让用户手动地输入对象标识看起来是一种很直接的作为,但这种方法也可能变得很复杂。复杂之一便是需要用户自己生产高质量的标识,此时标识可能是唯一的,也可能是不正确的。在大多数情况下唯一标识是不变的,但如果需要提供修改方法,就需要考虑后续的一系列验证问题。

应用程序生成唯一标识

有很多可靠的方法都可以自动生成唯一标识,但是如果在集群环境或者分布在不同的节点中,可以使用UUID或GUID来生成完全唯一的标识。

UUID是一种快速生成唯一标识的方法,不需要与外界进行交互。对于有性能要求的领域来说,我们可以将UUID实例缓存起来,使其在背后不间断地向缓存中填入新的UUID值。根据UUID能够表达实体的唯一程度,我们可以只使用UUID中的一部分来标记实体。

在**聚合(10)**边界之内,我们可以将缩短后的标识作为实体的本地标识。而另一方面聚合根的实体则需要全局的唯一标识。

持久化机制生成唯一标识

我们向数据库获取一个序列值或递增值,结果总是唯一的。根据标识的所需范围,数据库可以生成2字节、4字节和8字节的唯一标识。

性能可能是这个方法的一个缺点,从数据库中获取标识比直接从应用程序中生成标识要慢的多。如果可以使用延迟生成的方式,那么缓存标识就不是什么问题了。以mysql为例:

<id name="id" type="long" columm="product_id">
	<generator class="native"/>
</id>

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

如果另一个限界上下文用于给实体标识赋值,那么我们需要对每一个标识进行查找、匹配和赋值。其中最重要的是精确匹配,此时用户需要提供一种或多种属性,比如账户、email等。

在这种方式中,对象同步可能是个问题。外部对象的改变将如何影响本地对象?这个可以通过事件驱动架构领域事件予以解决。

标识生成时机

原文是时间,会容易出现误解是在生成标识的时候再保存当前生产时间,所以就做了调整

实体唯一标识的生成即可以发生在对象创建的时候,也可以发生在持久化对象的时候。在生成标识的时候,我们需要知道将哪些因素考虑在内。

考虑一下当客户端需要向外界发布领域事件的情形,在Product初始化完成之后,系统将产生一个领域事件,该事件将保存在事件存储中。最后所存储的事件将被外部限界上下文的订阅方所接收。此时就需要在初始化之时生成唯一标识。

还有个问题是,两个或多个实体需要加入Set集合中,此时没有标识会导致equals()方法比较出错导致其他实体被过滤。

委派标识

有些ORM工具会通过自己的方式处理对象的身份标识,此时如果我们自己的领域需要另一种实体标识,两者就会产生冲突。为了解决这个问题,我们需要两种标识,一种领域所使用,一种为ORM使用,这个就被成为委派标识。

此时直接在实体上创建一个属性来保存委派标识即可,同时在数据库中创建对应的列来保存,并加上主键约束。

对外界来说,我们最好将委派标识隐藏起来,因为委派标识并不是领域模型的一部分,而将委派标识暴露给外界可能造成持久化漏洞。此时我们可以使用层超类型

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;
    }
}

这里的IdentifiedDomainObject便是层超类型,这是一个抽象基类,通过protected关键字向客户端隐藏了委派主键。所有实体都扩展自这个抽象类。另外层超类型还有起来好处,比如支持乐观锁,参考聚合(10)

标识稳定性

在大多数情况下,我们都不应该修改实体的唯一标识,这样可以在实体的整个生命周期中保持标识的稳定性。

我们可以考虑将setter方法对客户端隐藏起来,也可以加一些断言确保标识在存在时不会被更新。

发现实体及其本质特征

限界上下文中的通用语言向我们提供了设计领域模型的概念术语。通用语言需要与领域专家详细讨论之后才能得出。

作者以SaaSOvation团队创建User模型为例,详细的分析了该实体模型的创建过程。通过对需求逐步澄清以及和相似实体Tenant做对比,从重要属性行为角色职责生命周期多个方面进行分析和验证,给出了创建User实体所需要进行的步骤。

此节不再具体展开描述。