用值对象表示标准类型
在许多的应用和系统中,都会使用到标准类型(Standard Type)。标准类型是用于表示事物类型的描述性对象。系统中既有表示事物的实体和描述实体的值对象,同时还存在标准类型来区分不同的类型。我并不知道工业上是否存在一个标准的名字来表达这样的概念,但是我却听说过类型码(type code)和查阅对象(look叩)。类型码并没有传达足够的信息,而查阅对象需要查询什么我们也不知道。为了使这个概念更加清晰,可以考虑几个用例场景。在有些场景下,这个概念被建模成了动力类型(Power Types)。
你的通用语言定义了一个PhoneNumber值对象,同时需要为每个 PhoneNumbei* 对象制定一个类型。“这个号码是家庭电话、移动电话、工作电话还是其他类型的电话号码?”不同类型的电话号码类型需要建模成一种类的层级关系吗?为每一个类型创建一个类对于客户端使用来说是非常困难的。此时,你需要的是使用标准类型来描述不同类型的电话号码,比如Home、Mobile、Work或者Other。
正如先前所讨论的,在一个金融领域中,我们需要一个Currency值对象来表示一个MonetaryValue对象的货币类型。在这个例子中,一个标准类型可以用于表示AUD、CAD、CNY、EUR、GBP、JPY和USD等货币类型。使用标准类型可以避免伪造货币。虽然一个不正确的货币类型可能被赋给MonetoryValue,但是一个不存在的货币类型是不能的。如果使用字符串属性来表示货币类型,那么便有可能导致一种不正确的状态。想想如果将表示美元的dollar拼写成rdoolar,结果会怎么样?
你也可能在制药行业里工作,你所开发的药剂具有不同的给药途径。某种药剂 (实体)可能拥有很长的生命周期,从概念设计、研究、开发、测试、生产、改进到终止使用。我们可以使用标准类型来管理这些不同的阶段,当然也可以使用不同的限界上下文来管理。另一方面,对于给药途径,使用标准类型便是更好的方法了,比如静脉注射、口服或者局部施用等。
根据标准化程度,这些类型可能只能用在应用程序级别,也或者可以在不同的系统间共享,更或者可以成为一种国际标准。
标准化程度有时会影响到对标准类型的获取,同时还有可能影响到标准类型在模型中的使用方式。
我们可以将这些概念建模成实体,因为它们在自己的限界上下文中都拥有自己的生命周期。在不考虑创建方式和由什么样的标准组织维护的情况下,在作为消费方的限界上下文中,我们应该尽可能地在将这些概念建模成值对象。这是一种很好的做法,因为这些概念本来就是用来度量和描述事物的,而值对象便是建模度量和描述概念的最佳方式。再者,一个{静脉注射}实例和另一个{静脉注射}表示的是相同的概念,它们是可以互换的,进而说明它们是可以相互代替的,并且可以进行相等性比较。因此,在限界上下文中,如果没有必要维护一个描述类型对象的生命周期,那么请将其建模成值对象。
为了维护方便,最好是为标准类型创建单独的限界上下文。在这样的上下文中,这些标准类型便是实体了,拥有持久化的生命周期,并且还含有属性,比如identity、name和description。可能还有其他属性,但是这里的3个属性对于消费上下文来说是最常见的。通常来说,我们只会使用其中一个属性,这也是最小化集成的目标。
作为一个简单例子,考虑一个表示两种成员类型的标准类型。成员类型分别为USER和GROUP (可以嵌套),在Java中可以使用枚举来表示该标准类型:
package com.saasovation.identityaccess.domain.model.identity;
public enum GroupMemberType {
GROUP {
public boolean isGroup() {
return true;
}
},
USER {
public boolean isUser() {
return true;
}
};
public boolean isGroup() {
return false;
}
public boolean isUser() {
return false;
}
}
一个GroupMember值对象实例在初始化时接受一个GroupMemberType标准类型。当一个User或一个Group被指派给了某个Group,完成指派操作的聚合应该创建正确的GroupMember:
protected GroupMember toGroupMember() {
GroupMember groupMember =
new GroupMember(this.tenantId(), this.username(), GroupMemberType.USER); // enum standard type
return groupMember;
}
Java的枚举是实现标准类型的一种简单方法。枚举提供了一组有限数量的值对象,它是非常轻量的,并且无副作用。但是该值对象的文本描述在什么地方呢?对于此,存在两种答案。通常来说,没有必要为标准类型提供描述信息,只需要名字就足够了。为什么?文本描述通常只在用户界面层(4)中才能用到,此时可以用一个显示资源和类型名字匹配起来。很多时候用于显示的文本都需要进行本地化(比如在多语言环境中),因此将这种功能放在模型中并不合适。通常来说,在模型中使用标准类型的名字便是最好的方式。另外一种答案是,在枚举中已经存在描述信息了,比如上面的USER和GROUP。你可以调用toStringO方法来获得标准类型的文本描述。
这个简单的由Java枚举所表示的标准类型也是一种优雅的状态[Gamma et al.] 对象。GroupMemberType的isGroupO和isUser()方法实现了所有状态的默认行为,即在默认状态下,这两个方法都返回false,这种默认行为是合适的。然后在每一个状态定义中,相应的方法被重载以表示正确的状态。当标准类型的状态为GROUP时,isGroupO方法将返回true;而当标准类型的状态为USER时,isUser()方法则将返回tme。状态的改变通过把当前枚举替换成另一个枚举来实现。
以上的枚举向我们展示了一些基本的行为,根据领域的需要,状态模式的实现可以变得非常复杂。本书中另一个重要的标准类型是Backl〇g〖temStatusType,其中包含了PLANNED、SCHEDULED、COMMITTED、DONE和REMOVED状态。在本书的3个示例限界上下文中,我们都将使用到这种标准类型。
状态模式是有害的?
有人并不看好状态模式,他们的一个抱怨是:状态模式需要创建一个抽象基类,其中包含了需要支持的所有行为(比如GrcmpMemberType的最后2个方法),然后为每个实际状态创建一个实体类来覆盖抽象类的行为。在Java中,我们需要为抽象类和实际状态类分别创建一个类(通常是一个类文件)。不管你是否喜欢这种做法,这就是状态糢式的工作方式。
我同意,为所有的状态创建单独的类会使系统变得复杂。对于实体状态类来说,有些行为来自于自身,有些行为继承自抽象基类,这一方面在子类和父类之间形成了紧耦合,另一方面使代码的可读性变差。但是,使用Java的枚举则是非常简单的,与通过状态模式来创建标准类型相比,枚举可能是更好的方法。我认为这里我们同时得到了两种方法的好处。一方面我们获得了一个非常简单的标准类型,另一方面又能有效地表示当前的状态。
如果你不喜欢使用Java的枚举来表示标准类型,你仍然可以使用某个值对象实例来表示。然而,如果你并不打算使用状态模式,那么枚举可能是最简单的方法。当然,除了枚举和状态模式之外,还有另外的方式可以实现标准类型。
我们还可以使用聚合来表示标准类型,其中每一个聚合实例代表一种类型。但是,此时我们需要慎重考虑。作为消费方的限界上下文并不会维护标准类型。被大范围使用的标准类型应该在一个单独的限界上下文中进行维护。在向消费上下文提供标准类型的聚合时,我们应该保证这些聚合的不变性。这时,我们需要思考,这个不变的聚合实体还是一个真正意义上的实体吗?如果不是,我们应该将其建模成一个共享的值对象实例。
一个共享的不变值对象可以从持久化存储中获取,此时可以通过标准类型的 领域服务(7)或工厂(11)来获取值对象。我们应该为每组标准类型创建一个领域服务或工厂(比如一个服务处理电话号码类型、一个服务处理邮寄地址类型,另一个服务处理货币类型),如图6.3所示。服务或工厂将按需从持久化存储中获取标准类型,而客户方并不知道这些标准类型是来自数据库中的。另外,使用领域服务或工厂还使得我们可以加入不同的缓存机制,由于值对象在数据库中是只读的,并且在整个系统中是不变的,缓存机制也将变得更加简单和安全。
总的来说,我还是建议尽量使用枚举来表示标准类型,即便你认为某个标准类型更像一种状态模式。如果存在大量的标准类型实例,我们可以考虑通过代码生成来创建枚举。例如,代码生成工具将从数据库中读取标准类型,然后为每一个标准类型(数据库中的一条记录)创建对应的枚举。
如果你打算用常规的值对象来表示标准类型,那么此时可以使用领域服务或工厂来静态地创建值对象实例。这种方式在动机上和前面的方法存在相似之处,但是在实现上却与共享值对象是不同的。在这种情况下,领域服务或工厂将为每一种标准类型提供静态创建的不变值对象实例。由于是静态的,数据库中标准类型的改变不会自动反映到代码中。如果你希望这两者是同步的,那么你应该创建一些定制化的方案来查询并更新模型的状态。这样做可能减少值对象作为标准类型所带来的好处4。因此,你可以在设计早期做出一个决定:这些静态创建的值对象在消费限界上下文中是永远不会被更新的。
4. 此时可以在上游上下文和下游上下文中分别创建聚合实体对象,它们不见得一定是相 同的类,但是我们应该保证这两种聚合之间的最终一致性。 ↩