教会你何时定义领域服务

标签 | DDD

字数 | 3232字

阅读 | 9分钟

若遵循基于面向对象设计范式的领域驱动设计,并用以应对纷繁复杂的业务逻辑,则强调领域模型的充血设计模型已成为社区不争事实。我将Eric提及的战术设计要素如Entity、Value Object、Domain Service、Aggregate、Repository与Factory视为设计模型。这其中,只有Entity、Value Object和Domain Service才能表达领域逻辑。

为避免贫血模型,在封装领域逻辑时,考虑设计要素的顺序为:

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,就更不可能了。

public class ContractRepositoryImpl implements ContractRepository {
    public Contract contractById(Identity contractId) {
        //这里并不知道Contract对象需要注入ContractRepository对象自身
    }
}

若真要解决此依赖管理问题,较简单的做法是为Contract提供一个setContractRepository()的依赖注入方法。不过,当Contract是通过Repository来获得时,如Spring、Guice之类的DI框架都无法注入这一依赖,因而需要显式调用,这就会引入对Repository具体实现的耦合。这样的耦合放在领域层,会导致本来单纯的领域层内核依赖了外部资源。倘若将这种具体耦合往外推,例如推到应用层,又会加重调用者的负担。

领域服务则不存在此问题,因为它的生命周期不是由Repository管理。如下的领域服务定义是合情合理的:

public class DuplicatedNumberChecker {
	
    @Repository
    private ContractRepository repository;
	
    public boolean isDuplicate(CustomizedNumber number) {
        return repository.existsNumber(number);
    }
}

我们在分配领域逻辑时,领域服务是最轻易也是最便宜的首选。这会导致领域服务的泛滥,长此以往,对领域层的开发又会走向“贫血模型”的老路。所谓“服务”本身就是一个抽象概念。越抽象就越显得包容并蓄。例如定义一个OrderService,那么所有和订单有关的逻辑都可以往这个服务里面塞,而诸如Order之类的实体对象终归有不少限制,分配职责时需得思虑再三。因此,倘若在设计与开发时对职责的分配不加约束,所谓的“职责分治”就不过是一句空话罢了。

归根结底,主流的领域驱动设计在战术层面考察的其实是面向对象的设计能力。我认为,所谓面向对象设计,核心就是角色职责协作。在分配职责时,应考虑将数据与行为封装在一起,这是面向对象设计的首要原则。

为了避免程序员把领域服务当做一个“筐”,什么逻辑都往里面装,除了需要提高团队成员面向对象的设计能力,并加强代码评审之外,还有一个方法,就是对领域服务加以约束。

没有任何语言可以在DDD设计要素上施加约束。Mat Wall与Nik Silver在对Guardian.co.uk网站推行DDD时的实践值得我们借鉴。他们在文章《演进架构中的领域驱动设计》中建议:

为了对付这一行为,我们对应用中的所有服务进行了代码评审,并进行重构,将逻辑移到适当的领域对象中。我们还制定了一个新的规则:任何服务对象在其名称中必须包含一个动词。这一简单的规则阻止了开发人员去创建类似于ArticleService的类。取而代之,我们创建 ArticlePublishingService和ArticleDeletionService这样的类。推动这一简单的命名规范的确帮助我们将领域逻辑移到了正确的地方,但我们仍要求对服务进行定期的代码评审,以确保我们在正轨上,以及对领域的建模接近于实际的业务观点。

其实,这一别具一格的约束形式其实与服务的本质是一脉相承的,即服务应代表无状态的领域行为,甚至可以说领域服务是领域层面用例的体现。

这一实践可能会导致更多细粒度的领域服务产生,但更有可能的结果是,当我们在创建一个新的领域服务时,可能会考虑暂时停下来,想一想,要分配给这个新服务的领域逻辑是否有更好的去处呢?即使因为该逻辑可能牵涉到多个领域实体,又或者需要与Repository协作而不得不放入到领域服务中,似乎也可以考虑将领域逻辑中与实体(或值对象)数据强相关的内容”摘“出来,分配到合适的地方,保证职责分配的合理均衡。和谐的协作机制是好的面向对象设计。

本文链接: http://zhangyi.xyz/example-of-define-domain-service/


❈ 题图为意大利插画家Andrea Ucini马蒂斯有花瓶的一副作品,来自Mono的《插画太空馆》。

原文发布于微信公众号 - 逸言(YiYan_OneWord)

原文发表时间:2018-05-11

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏杨建荣的学习笔记

从设计模式的设计原则感悟生活(r2笔记42天)

设计模式中的很多思想还是很有意思的,刚毕业的时候接触设计模式感觉有点高深,坐而论道,感觉还是有些虚,平时做的小练习还能自己捣鼓一番,自己使用一下设计模式,然后大...

34460
来自专栏SDNLAB

SDN实战团分享(二十):From Lithium to Beryllium,ODL最新动态变化

大家好, 感谢宇峰的邀请, 和大家分享OpenDaylight最新发布的一些进展,胶片主要来自Neela Jacques, Phil Robbs和Colin D...

28540
来自专栏TEG云端专业号的专栏

【Augustzhang 张元龙】知根知底,方能游刃有余

小编语:据江湖传闻,龙哥从初中就开始写代码,高中通过计算机竞赛免试上了大学,大学里则是ACM大神。2010年毕业加入腾讯,先后从事密保、验证码等后台研发工作,...

37040
来自专栏SDNLAB

SDN实战团分享(二十):From Lithium to Beryllium,ODL最新动态变化

家好, 感谢宇峰的邀请, 和大家分享OpenDaylight最新发布的一些进展,胶片主要来自Neela Jacques, Phil Robbs和Colin Di...

35670
来自专栏企鹅号快讯

Python的新手指南,教你如何变编程大佬级别的

新的Python?下面是基础知识的细分,包括语言的历史,使用者的语言以及Python 2与3的区别。 Python是一种编程语言,对于编写快速而简单的脚本非常有...

24780
来自专栏java达人

如何编写复杂sql

经常有人问我那非常复杂的sql是怎么写出来的,我一直不知道该怎么回答。 因为虽然我写这样的sql很顺手,可是我却不知道怎么告诉别人怎么写。很多人将这个问题...

90790
来自专栏编程微刊

2017年10大主流编程语言最新排行榜出炉

42930
来自专栏程序你好

开发有效地 Java微服务需要Effective Java

编写好的软件需要使用正确的工具。选择正确的框架、库和设计“聪明”的系统。因为有这些东西需要学习和担心,很容易忘记另外一件非常重要的事情:明智地选择使用编程语言。...

8620
来自专栏Java学习网

编程能力七段论

编程能力七段论 前言   程序员的编程技能随着经验的积累,会逐步提高。我认为编程能力可以分为一些层次。   下面通过两个维度展开编程能力层次模型的讨论。   一...

37550
来自专栏JavaQ

码农福利(二)

DUBBO是阿里巴巴的开源分布式服务框架,致力于提供高性能和透明化的RPC远程服务调用方案,是阿里巴巴SOA服务化治理方案的核心框架,每天为2,000+个服务...

33570

扫码关注云+社区

领取腾讯云代金券