若遵从根据面向目标规划范式的范畴驱动规划,并用以应对纷繁复杂的事务逻辑,则着重范畴模型的充血规划模型已成为社区不争现实。我将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,就更不或许了。
若真要处理此依靠办理问题,较简略的做法是为Contract供给一个setContractRepository()的依靠注入办法。不过,当Contract是经过Repository来获得时,如Spring、Guice之类的DI结构都无法注入这一依靠,因而需求显式调用,这就会引进对Repository详细完成的耦合。这样的耦合放在范畴层,会导致原本单纯的范畴层内核依靠了外部资源。假使将这种详细耦合往外推,例如推到使用层,又会加剧调用者的担负。
范畴服务则不存在此问题,由于它的生命周期不是由Repository办理。如下的范畴服务界说是入情入理的:
咱们在分配范畴逻辑时,范畴服务是最容易也是最廉价的首选。这会导致范畴服务的众多,久而久之,对范畴层的开发又会走向“贫血模型”的老路。所谓“服务”自身便是一个笼统概念。越笼统就越显得容纳并蓄。例如界说一个OrderService,那么一切和订单有关的逻辑都能够往这个服务里边塞,而比如Order之类的实体目标终归有不少束缚,分配责任时需得思虑一再。因而,假使在规划与开发时对责任的分配不加束缚,所谓的“责任分治”就不过是一句废话算了。
归根到底,干流的范畴驱动规划在战术层面调查的其实是面向目标的规划能力。我以为,所谓面向目标规划,中心便是人物、责任与协作。在分配责任时,应考虑将数据与行为封装在一起,这是面向目标规划的首要准则。
为了防止程序员把范畴服务作为一个“筐”,什么逻辑都往里边装,除了需求进步团队成员面向目标的规划能力,并加强代码评定之外,还有一个办法,便是对范畴服务加以束缚。
没有任何言语能够在DDD规划要素上施加束缚。Mat Wall与Nik Silver在对Guardian.co.uk网站推广DDD时的实践值得咱们学习。他们在文章《演进架构中的范畴驱动规划》中主张:
为了抵挡这一行为,咱们对使用中的一切服务进行了代码评定,并进行重构,将逻辑移到恰当的范畴目标中。咱们还拟定了一个新的规矩:任何服务目标在其称号中有必要包括一个动词。这一简略的规矩阻挠了开发人员去创立类似于ArTIcleService的类。取而代之,咱们创立 ArticlePublishingService和ArticleDeletionService这样的类。推进这一简略的命名标准确实协助咱们将范畴逻辑移到了正确的当地,但咱们仍要求对服务进行定时的代码评定,以确保咱们在正轨上,以及对范畴的建模接近于实践的事务观念。
其实,这一独具匠心的束缚方式其实与服务的实质是一脉相承的,即服务应代表无状况的范畴行为,乃至能够说范畴服务是范畴层面用例的表现。
这一实践或许会导致更多细粒度的范畴服务发生,但更有或许的结果是,当咱们在创立一个新的范畴服务时,或许会考虑暂时停下来,想一想,要分配给这个新服务的范畴逻辑是否有更好的去向呢?即便由于该逻辑或许牵涉到多个范畴实体,又或许需求与Repository协作而不得不放入到范畴服务中,好像也能够考虑将范畴逻辑中与实体(或值目标)数据强相关的内容”摘“出来,分配到适宜的当地,确保责任分配的合理均衡。调和的协作机制是好的面向目标规划。