专栏名称: 逸言
文学与软件,诗意地想念。
目录
相关文章推荐
程序员的那些事  ·  DeepSeek 下棋靠忽悠赢了 ... ·  19 小时前  
OSC开源社区  ·  继V3之后,沐曦GPU再完成DeepSeek ... ·  昨天  
程序员小灰  ·  这款AI编程工具,将会取代Cursor! ·  2 天前  
码农翻身  ·  为何 Linus ... ·  2 天前  
51好读  ›  专栏  ›  逸言

教会你何时定义领域服务

逸言  · 公众号  · 程序员  · 2018-05-11 09:25

正文

标签 | 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)协作

假设某系统的合同管理功能允许客户输入自编码,该自编码需要遵循一定的编码格式。在创建新合同时,客户输入自编码,系统需要检测该自编码是否在已有合同中已经存在。针对该需求,可以提炼出两个领域行为:

  • 验证输入的自编码是否符合业务规则

  • 检查自编码是否重复

在寻找职责的履行者时,我们应首先遵循“ 信息专家模式 ”,即“拥有信息的对象就是操作该信息的专家”,因此可以提出一个问题:领域行为要操作的数据由谁拥有?针对第一个领域行为,就是要确认谁拥有自编码格式的验证规则?有两个候选:

  • 拥有自编码信息的“合同(Contract)”对象

  • 体现自编码知识概念自身的“自编码(CustomizedNumber)”对象

我倾向于定义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对象需要注入ContractRepository对象自身
   }
}

若真要解决此依赖管理问题,较简单的做法是为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之类的实体对象终归有不少限制,分配职责时需得思虑再三。因此,倘若在设计与开发时对职责的分配不加约束,所谓的“职责分治”就不过是一句空话罢了。







请到「今天看啥」查看全文