当前位置: 首页 > 科技观察

教你什么时候定义领域服务

时间:2023-03-19 02:03:21 科技观察

如果你遵循基于面向对象设计范式的领域驱动设计,并用它来处理复杂的业务逻辑,那么强调领域模型的充血设计模型就成了不争的事实社区中的事实。我把Eric提到的战术设计元素Entity、ValueObject、DomainService、Aggregate、Repository和Factory作为设计模型。其中只有Entity、ValueObject和DomainService可以表达领域逻辑。为了避免模型贫血,在封装领域逻辑时,设计元素的顺序应该考虑:ValueObject->Entity->DomainService记住,一定要用领域服务作为承接业务逻辑的最后一根稻草。DomainService之所以被放在***,是因为我太了解DomainService的强大“魔力”了。开发者总是有一种惰性,很多时候不愿仔细思考所谓“责任(封装领域逻辑的行为)”的正确执行者,而领域服务恰恰是最方便的选择。据我理解,只有满足以下三个特征的领域行为才应该放在领域服务中:领域行为需要多个领域实体参与协作领域行为与状态无关领域行为需要与外部资源(尤其是DB)进行协作假设某系统的合同管理功能允许客户输入自己的代码,需要遵循一定的编码格式。在创建新合同时,客户输入自己的代码,系统需要检测自己的代码是否已经存在于已有的合同中。针对这个需求,可以抽取两个领域行为:验证输入的自编码是否符合业务规则检查自编码是否重复在寻找责任执行者时,首先要遵循“信息专家模式””,即“拥有信息的对象是操纵信息的专家”,因此可以提出一个问题:谁拥有域行为要操纵的数据?对于第一个域行为,就是确认谁拥有自编码格式中的验证规则?有两个候选:自己的自编码信息的“Contract”对象体现了自编码知识概念本身的“CustomizedNumber”对象,我倾向于定义CustomizedNumber值对象,将检测规则封装在其中,在构造函数中实现验证。在领域驱动设计中,值对象常常被用来封装这些基本概念。由于自定义类型可以封装领域行为,可以有效实现职责的“分而治之”,实现对象的协作。如果要查看自编码是否重复,需要从数据库中查找,需要通过Repository与DB配合。基于上面总结的三个特点,这个职责应该分配给一个领域服务,比如DuplicatedNumberChecker。从责任分配的角度来看,实体Contract或者价值对象CustomizedNumber应该是承担这个责任的合理选择。我为什么要定义这样一个例外原则?原因是在领域驱动设计中,尽量保证实体和值对象的纯洁性,尤其是不要依赖Repository(资源库)。继续深挖根源,因为实体和值对象的生命周期是由Repository管理的。如果被管理的实体对象也依赖于Repository,要求实体对应的Repository来管理实体对象的生命周期及其对Repository的依赖是不合理的。出于同样的原因,值对象位于聚合边界内。例如,假设Contract是聚合根,如果将检查重复编码的职责分配给实体对象(或值对象CustomizedNumber),则内部需要依赖ContractRepository。但是,合约也是通过Repository获取的。基础设施层在实现ContractRepository时,并不知道如何管理两者之间的依赖关系。如果Contract实体依赖于其他Repositories,那就更不可能了。publicclassContractRepositoryImplimplementsContractRepository{publicContractcontractById(IdentitycontractId){//不知道Contract对象需要自己注入ContractRepository对象}}如果真的要解决这个依赖管理问题,更简单的方法是提供一个setContractRepository()Contract的依赖注入方法。但是通过Repository获取Contract时,Spring、Guice等DI框架无法注入这种依赖,需要显式调用,这会引入对Repository具体实现的耦合。这样的耦合放在领域层,会造成原本纯领域层的核心要依赖外部资源。如果把这种特定的耦合推到外面,比如推到应用层,就会增加调用者的负担。领域服务不存在这个问题,因为它的生命周期不是由Repository管理的。下面的领域服务定义是合理的:publicclassDuplicatedNumberChecker{@RepositoryprivateContractRepositoryrepository;publicbooleanisDuplicate(CustomizedNumbernumber){returnrepository.existsNumber(number);}}当我们分配领域逻辑时,领域服务是最简单高效的***。这将导致领域服务的激增。长此以往,领域层的开发就会走上“贫血模型”的老路。所谓“服务”本身就是一个抽象的概念。越抽象就越显得包容。比如你定义了一个OrderService,那么所有与订单相关的逻辑都可以塞进这个服务中,而Order这样的实体对象毕竟有很多限制,所以在分配职责的时候需要三思而后行。因此,如果在设计和开发时不限制职责分配,所谓的“职责分离”只是一句空话。归根结底,主流的领域驱动设计考察的是战术层面的面向对象设计能力。在我看来,所谓面向对象设计的核心就是角色、职责和协作。在分配职责时,要考虑数据和行为的封装,这是面向对象设计的首要原则。为了防止程序员把领域服务当成一个“篮子”,把所有的逻辑都放在里面,除了提高团队成员的面向对象设计能力,加强代码审查之外,还有一个方法,就是对域服务。没有语言可以对DDD设计元素施加约束。我们可以借鉴MatWall和NikSilver在Guardian.co.uk网站上实现DDD的实践。在他们的文章《演进架构中的领域驱动设计》中,他们建议:为了对抗这种行为,我们对应用程序中的所有服务进行了代码审查,并重构它们以将逻辑移动到适当的域对象中。我们还制定了一条新规则:任何服务对象的名称中都必须包含一个动词。这个简单的规则会阻止开发人员创建像ArticleService这样的类。相反,我们创建像ArticlePublishingService和ArticleDeletionService这样的类。推动这个简单的命名约定确实帮助我们将领域逻辑移到了正确的位置,但我们仍然需要定期对服务进行代码审查,以确保我们走上正轨,并且领域的建模接近实际业务视图。事实上,这种独特的约束形式其实很符合服务的本质,即服务应该代表无状态的领域行为,甚至可以说领域服务是领域级用例的体现。这种做法可能会导致更细粒度的领域服务,但更可能的结果是,当我们创建一个新的领域服务时,我们可能会考虑暂时停止并考虑一下。服务的域逻辑是否有更好的地方?即使逻辑因为可能涉及到多个领域实体,或者需要配合Repository而不得不放到领域服务中,貌似可以考虑将领域逻辑与实体(或值对象)数据强相关的内容结合起来被“提取”并分配到适当的地方,以确保合理和平衡的责任分配。和谐的协作机制是很好的面向对象设计。【本文为专栏作家“张艺”原创稿件,转载请联系原作者】点此阅读更多该作者好文