唯一标识

在实体设计早期,我们将刻意地把关注点放在能体现实体身份唯一性的主要属性和行为上,同时还将关注如何对实体进行查询。另外,我们还会刻意地忽略掉那些次要的属性和行为。

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

实体的唯一标识并不见得一定有助于对实体的查找和匹配。将唯一标识用于实体匹配通常取决于标识的可读性。比如,如果系统提供人名查找功能,但此时一个person实体的唯一标识极有可能不是人名,应为存在大量重名的情况。另一方面,如果一个系统童工根据公司税号的查找功能,此时税号便可以作为Company实体的唯一标识,因为政府为每个公司分配了唯一的税号。

值对象可以用来存放实体的唯一标识。值对象是不变的(immutable)的,这可以保证实体身份的稳定性,并且与身份标识相关的行为也可以得到集中处理。

创建实体身份标识的策略

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

用户提供唯一标识

这是一种简单的方法,但是这种方法也可能变得复杂。

复杂性之一便是需要用户自己生成高质量的标识。此时标识可能是唯一的,但却有可能是不正确的。在多数情况下,标识必须是不变的,因此用户不能对标识进行修改。但是情况并不总是如此,有时赋予用户修改标识值的权力是有好处的。

应用程序生成唯一标识

有很多可靠的方法都可以自动生成唯一标识,但是如果应用程序处于集群环境或者分布在不同的计算节点中,我们就需要额外小心了。

有些方法可以生成完全唯一的标识,比如UUID或者GUID。以下是生成唯一标识的另一种方法,其中每一步生成的结果都将添加到最终的文本标识中:

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

持久化机制生成唯一标识

将唯一标识的生成委派给持久化机制是有特别的好处的。如果我们向数据库获取一个序列值(Sequence)或递增值,结果总是唯一的。

标识生成时间

实体唯一标识的生成既可以发生在对象创建的时候,也可以发生在持久化对象的时候。有时我们需要及早地生成实体标识,而有时标识生成时间则不那么重要。

发现实体及其本质特征

挖掘实体的关键行为

在识别出实体的重要属性之后,SaaSOvation团队开始转向实体的行为。。。。

当我们思考激活(Activate)或禁用(Deactivate)一个Tenant时,我们想可能是一个布尔开关,至于如何实现这个开关并不重要。如果我们将一个activate属性添加到Tenant类图中,别人在看到这张类图时,他/她能知道activate表示什么意思吗?

  1. public class Tenant extends Entity {
  2. ...
  3. private boolean active;
  4. ...
  5. }

上面的activate恐怕并不能完全表达出它的意图。在开始的时候,我们将关注点在对身份和查询有用的属性上,之后我们希望通过相似的方法加入一些与服务相关的信息。

团队也许会定义一个setActive(boolean)的方法,虽然这个方法并不能很好地表达需求术语。这里并不是说共有的setter方法不合适,而是说只有在符合通用语言的情况下才能使用setter方法,也或者,只有当我们不必使用多个setter方法来完成单个请求时,才有道理使用setter方法。多个setter方法使意图充满歧义,同时也使发布领域事件变得复杂,因为一个领域事件应该对应与逻辑上的单个命令。

考虑到通用语言,团队成员意识到领域专家使用的是“激活”和“冻结”这两个动作。为了准确地体现这些术语,它们将setter方法改成了activate()和deactivate()方法。

  1. public class Tenant extends Entity {
  2. ...
  3. public void activate() {
  4. // TODO:实现
  5. }
  6. public void deactivate() {
  7. // TODO:实现
  8. }
  9. }

领域对象扮演多种角色

在面向对象编程中,通常由接口来定义实现类的角色。在正确设计的情况下,一个类对于每一个它所实现的接口来说,都存在一种角色。如果一个类没有显式的角色—-即该类没有实现任何显式接口,那么在默认情况下它繁衍的即是本类的角色。也即,该类的共有方法标识该类的隐式接口。比如,上面的User类并没有实现任何接口,但是他依然扮演了一种角色,即user角色。

创建实体

当我们创建一个实体时,我们希望通过构造函数来初始化足够多的实体状态,这一方面有助于表面该实体的身份,另一方面可以帮助客户端更容易地查找该实体。在使用及早生成唯一标识的策略时,构造函数至少需要角色一个唯一标识作为参数。如果我们还有可能通过其他方式对实体进行查找,比如名字或描述信息,那么我们应该将这些参数也一并传给构造函数。

有时一个实体维护了一个或多个不变条件(Invariant)。不变条件即是在整个实体声明周期中都必须保持事务一致性的一种状态。不变条件主要是聚合所关注的,但是由于聚合跟通常也是实体,故这里我们也稍作提及。如果实体的不变条件要求改实体所包含的对象都不能为null状态,或者由其他状态计算所得,那么这些状态需要作为参数传递给构造函数。

每一个User对象都必须包含tenantId、username、password和person属性。换句话说,在User对象得到正确实例化之后,这些属性绝对不能为null。

验证

验证的主要目的在于检查模型的正确性,检查的对象可以是某个属性,也可以是整个对象,甚至是多个对象的组合。我们将对模型进行三个级别的验证。

验证可以达到不同的目的。即便领域对象的各个属性都是合法的,这也并不表示该对象作为一个整体是合法的。两个合法的属性组合起来有可能使整个对象不合法。同样的道理,单个对象的合法性并不能保证对象组合的合法性。两个合法实体对象的组合有可能是不合法的。因此我们需要采用不同级别的验证来处理这些情况。

验证属性

我们如何确保属性处于合法状态呢?我强烈建议使用自封装来验证属性。

Martin Fowler曾说

自封装性要求无论哪种方式访问数据,即使从对象内部访问数据,都必须通过getter和setter方法

这种方式有诸多优点。首先它为对象的实例变量和类变量提供了一层抽象。其次,我们可以方便地在对象中访问其所引用对象的属性。重要的是,自封装性验证变得非常简单。

事实上,我并不愿意将自封装性成为验证。在一些开发看来,验证是一个单独的关注点,因此应该讲该职责放在验证类上,而不是领域对象上。

  1. public final class EmailAddress {
  2. private String address;
  3. public EmailAddress(String anAddress) {
  4. super();
  5. this.setAddress(anAddress);
  6. }
  7. private void setAddress(String anAddress) {
  8. if (anAddress == null) {
  9. throw new IllegalArgumentException("The address may not be set to null");
  10. }
  11. this.address = anAddress;
  12. }
  13. }

验证整体对象

虽然有时实体中的所有单个属性都是合法的,但是这并不意味着整个实体就是合法的。要验证整个实体,我们需要访问整个对象的状态———所有对象属性。

由于验证逻辑需要访问实体的所有状态,有人可能会直接将逻辑嵌入到实体对象中。这里我们需要注意了,更多的时候验证逻辑比领域对象本身变化还快,而将验证逻辑嵌入在领域对象中也使领域对象承担了太多的职责。

此时我们可以创建一个单独的组件来完成模型验证。在Java中设计单独的验证类时,我们可以将该类放在和实体相同的模块中,将属性的getter方法声明在包级别(即用protect修饰),这样验证类便能访问到这些属性了。

验证类可以实现规范模式或策略模式。当发现非法状态时,验证类将通知客户方或者记录验证结果以便后用。验证过程应该收集到所有的验证结果,而不是在一开始遇到非法状态时就抛出异常。

  1. public abstract class Validtor {
  2. private ValidationNotificationHandler notificationHandler;
  3. ...
  4. public Validator(ValidationNotificationHandler notificationHandler) {
  5. super();
  6. this.notificationHandler;
  7. }
  8. public abstract void validate();
  9. protect ValidationNotificationHandler notificationHandler() {
  10. return this.notificationHandler;
  11. }
  12. private void setNotificationHandler(ValidationNotificationHandler notificationHandler) {
  13. this.notificationHandler = notificationHandler;
  14. }
  15. }
  16. public class WarbleValidator extends Validator {
  17. private Warble warble;
  18. public Validator(Warble warble, ValidationNotificationHandler notificationHandler) {
  19. super(notificationHandler);
  20. this.warble = warble;
  21. }
  22. ...
  23. public void validate() {
  24. if (this.hasWarpedWarbleCondition(this.warble())) {
  25. this.notificationHandler.handleError("The warble is warped.");
  26. }
  27. }
  28. }

验证对象组合

正如Ward Cunningham所说,在需要对复杂对象进行验证时,我们可以使用延迟验证。这里我们关注的并不只是某个单独的实体是否合法,而是多个实体的组合是否全部合法,包括一个或多个聚合实例。

但是,最好的方式是把这样的验证过程创建成一个领域服务。该领域服务可以通过资源库读取那些需要验证的聚合实例,然后对每个实例进行验证,可以是单独验证,也可以是和其他聚合组合起来验证。

跟踪变化

根据实体的定义,我们没有必要再整个生命周期中对状态的变化进行跟踪,而是只需要跟踪那些持续改变的状态。然而,有时领域专家可能会关心发生在模型中一些重要事件,此时我们便应该对实体的一些特殊变化进行跟踪了。