标签
| DDD
字数
| 3232字
阅读
| 9分钟
若遵循基于面向对象设计范式的领域驱动设计,并用以应对纷繁复杂的业务逻辑,则强调领域模型的充血设计模型已成为社区不争事实。我将Eric提及的战术设计要素如Entity、Value Object、Domain Service、Aggregate、Repository与Factory视为
设计模型
。这其中,只有Entity、Value Object和Domain Service才能表达领域逻辑。
为避免贫血模型,在封装领域逻辑时,考虑设计要素的顺序为:
1
|
Value Object -> Entity -> Domain Service
|
切记,我们必须将Domain Service作为承担业务逻辑的最后的救命稻草。之所以把Domain Service放在最后,是因为我太清楚领域服务的强大“魔力”了。开发人员总会有一种惰性,很多时候不愿意仔细思考所谓“职责(封装领域逻辑的行为)”的正确履行者,而领域服务恰恰是最便捷的选择。
就我个人的理解,
只有满足如下三个特征的领域行为才应该放到领域服务中
:
-
领域行为需要多个领域实体参与协作
-
领域行为与状态无关
-
领域行为需要与外部资源(尤其是DB)协作
假设某系统的合同管理功能允许客户输入自编码,该自编码需要遵循一定的编码格式。在创建新合同时,客户输入自编码,系统需要检测该自编码是否在已有合同中已经存在。针对该需求,可以提炼出两个领域行为:
-
验证输入的自编码是否符合业务规则
-
检查自编码是否重复
在寻找职责的履行者时,我们应首先遵循“
信息专家模式
”,即“拥有信息的对象就是操作该信息的专家”,因此可以提出一个问题:领域行为要操作的数据由谁拥有?针对第一个领域行为,就是要确认谁拥有自编码格式的验证规则?有两个候选:
我倾向于定义CustomizedNumber值对象,将该检测规则封装其内,并在构造函数中对其进行验证。在领域驱动设计中,值对象往往用于封装这些基础概念。由于自定义的类型可以封装领域行为,就可以有效地实现职责的“分治”,实现对象的协作。
若要检查自编码是否重复,则需要从数据库中查找,这就需要通过Repository与DB协作。基于前面总结的三个特征,则该职责应该分配给一个领域服务,例如DuplicatedNumberChecker。
从职责分配的角度看,实体Contract又或者值对象CustomizedNumber才应该是承担该职责的合理选择。为何我却定义了这么一条例外原则呢?究其原因,就是在领域驱动设计中,我们应尽量保证实体与值对象的纯粹性,尤其不应该依赖于Repository(资源库)。继续深挖根本原因,是因为
实体与值对象的生命周期是由Repository管理的
。倘若被管理的实体对象还依赖了Repository,就要求该实体对应的Repository在管理实体对象的生命周期的同时,还需要管理它与Repository的依赖,这并不合理。值对象在一个聚合(Aggregate)边界之内,道理相同。
举例来说,假设Contract是聚合根,如果将检查重复编码的职责分配给该实体对象(或值对象CustomizedNumber),内部就需要依赖ContractRepository。然而,Contract的获取也是通过Repository得到,在基础设施层对ContractRepository的实现时,其实
并不知道该如何管理二者之间的依赖
。如果Contract实体还要依赖其他Repository,就更不可能了。
1 2 3 4 5
|
public class ContractRepositoryImpl implements ContractRepository { public Contract contractById(Identity contractId) { } }
|
若真要解决此依赖管理问题,较简单的做法是为Contract提供一个
setContractRepository()
的依赖注入方法。不过,当Contract是通过Repository来获得时,如Spring、Guice之类的DI框架都无法注入这一依赖,因而需要显式调用,这就会引入对Repository具体实现的耦合。这样的耦合放在领域层,会导致本来单纯的领域层内核依赖了外部资源。倘若将这种具体耦合往外推,例如推到应用层,又会加重调用者的负担。
领域服务则不存在此问题,因为它的生命周期不是由Repository管理。如下的领域服务定义是合情合理的:
1 2 3 4 5 6 7 8 9
|
public class DuplicatedNumberChecker { @Repository private ContractRepository repository; public boolean isDuplicate(CustomizedNumber number) { return repository.existsNumber(number); } }
|
我们在分配领域逻辑时,领域服务是最轻易也是最便宜的首选。这会导致领域服务的泛滥,长此以往,对领域层的开发又会走向“贫血模型”的老路。所谓“服务”本身就是一个抽象概念。越抽象就越显得包容并蓄。例如定义一个OrderService,那么所有和订单有关的逻辑都可以往这个服务里面塞,而诸如Order之类的实体对象终归有不少限制,分配职责时需得思虑再三。因此,倘若在设计与开发时对职责的分配不加约束,所谓的“职责分治”就不过是一句空话罢了。