走向卓越,领域驱动设计的思维方式
作者:程序之心丁仪
来源:https://chengxuzhixin.com/blog/post/ling_yu_qu_dong_she_ji_de_si_wei_fang_shi.html
软件系统是以特定的代码解决现实世界的复杂问题。软件开发的最大困难就是应对复杂度,复杂度可能来源于各个方面。领域驱动设计的概念是 2004 年 Evic Evans 提出的 Domain-Driven Design,简称 DDD。随着软件技术发展,大家逐渐意识到领域驱动设计的重要性。
领域驱动设计是一套方法论,是从业务视角对需求的分析,指导我们将复杂问题进行拆分,帮助我们解决大型复杂系统在落地中遇到的问题。领域驱动设计,要从繁杂的需求中找出确定的领域模型。基于统一明确的领域模型,在需求、产品、开发、测试等团队之间能够形成【统一语言】,降低需求对焦的成本。
领域驱动的核心概念
领域驱动设计提出的核心概念包括:域、子域、限界上下文、领域实体、值对象、领域服务、领域事件、聚合、工厂、资源库等。
领域实体是指我们使用唯一的标识符跟踪具有重要业务意义的对象。实体代表的业务对象不变,但是会产生状态和业务属性的变更。实体一般和主要的业务/领域对象有一个直接的关系。一个实体的基本概念是一个持续抽象的生命,可以变化不同的状态和情形,但总是有相同的标识。
值对象用来描述特征。值对象不需要唯一标识,通常只关心具体的属性。值对象用来表示临时的事物,或者实体的属性。如果两个对象的所有的属性值都相同,可以认为它们是同一个对象的话,那么我们就可以把这种对象设计为值对象。
领域服务代表的是一些商业逻辑或业务处理过程,并不和领域对象相关,涉及的领域概念通常不属于一个实体或者值对象。领域服务是无状态的,在领域服务中实现领域逻辑的调用。它存在的意义就是协调领域对象共同完成某个操作,所有的状态还是都保存在相应的领域对象中。
领域事件代表的是系统中发生了什么,是一种特殊的值对象。领域事件由实体触发,通过事件解耦领域模型内部的一些依赖,会对领域模型中的其他部分产生业务影响。并不是所有的事件都是领域事件,领域事件必须对业务有价值,有助于形成完整的业务闭环,将导致进一步的业务操作。
根据业务需要可以聚合实体和值对象,并围绕聚合定义边界。聚合作为一个整体来定义属性,选择一个实体作为聚合的根对象,只允许外部对象引用聚合的根对象。聚合内对象具有一致的生命周期,一旦聚合根对象消失,则聚合内其他对象也要消失。
领域驱动设计使用了工厂模式来构造聚合对象。工厂方法只能返回聚合对象,不能返回聚合内细分的实体和值对象,需要确保外界对聚合的引用一定是通过根对象实现的。通过工厂来分离聚合的构造和使用,保证业务一致性。
聚合的仓储通过资源库来实现。外部系统只能通过资源库访问聚合。一个聚合只能有一个资源库对象,也就是以聚合根对象命名的资源库,不能再提供其他的资源库。
域和子域的划分是为了能够对复杂问题分而治之。在系统化思维中,特别强调系统中部分的划分和部分之间关系的处理。域的概念可以理解为系统化思维的一种实践。划分域后,可以简化认知、隔离域的变化、聚焦重点。一般通过观察业务流程、观察领域模型、观察分析过程,从业务视角出发来划分业务能力,实现域的定义和划分。
限界上下文是一个显式的边界,和子域的边界保持一致,如用户上下文(对应用户域)、支付上下文(对应支付域)。限界上下文能够明确领域模型的清晰的、达成共识的边界。边界不清晰常常引起诸多问题,如业务需求由哪个团队开发、模型定义放在哪个域。
领域驱动的架构设计
在一般的架构设计中,架构分层具有依赖关系。上层一定依赖于下层,下层为上层提供接口和服务。而在领域驱动设计中,推荐六边形架构来强化领域模型的重要地位。让外界系统服务于领域模型,实现领域内部的确定性。领域内部关注如何通过模型及其相关概念,在抽象的层次上把业务表达出来。
领域模型定义接口,由其他层实现接口。整体的设计完全围绕领域模型进行。一般架构设计中都把基础设施作为最底层,并且通常认为基础设施具有确定性。而在领域驱动设计中,对于基础设施(如数据持久化),由领域模型定义接口并由基础设施层实现接口。这样的设计保证领域模型对抽象有确定的定义,而基础设施作为六边形外围接口的实现可以随时替换。
领域驱动的事件风暴
领域模型的定义是在需求分析过程中产生的。需求分析一般使用被称为【事件风暴】的分析方法。使用领域驱动设计的理念去做需求分析,意味着我们将把事件作为项目的中心。通过【事件风暴】可以发现完整的业务场景和潜在的遗漏点,可以建立端到端的共识,能够帮助梳理出测试用例。
事件的发生一般会对领域对象产生影响。因此【事件风暴】的分析步骤一般包括:
- 首先确定终态业务事件,比如打车已经到达目的地;
- 然后从终态业务事件出发,反推达成此事件需要的前提条件,从而确定业务的事件流;
- 再调整流程和补充分支事件,比如加上乘客迟到意外事件;
通过【事件风暴】可以同步产出领域模型。任何在需求中出现的概念都要反应在领域模型中。如果需求中概念之间存在关系,则领域模型中也存在同样的关系。因此需求分析完成后,领域模型是自然而然产生的,统一语言也有了实现的基础。
通过【事件风暴】还可以梳理出完整的业务场景。在显式明确领域模型后,常常可以发现模糊的需求或者产生新需求,能够明确看到需求的遗漏点。梳理出完整的场景后,测试用例也能同步产出,进一步在各个团队之间达成共识。
领域驱动的开发思维
除了前面提到的概念外,领域驱动设计还影响了开发思维,也就是由外而内和测试先行。
由外而内是指开发代码从外部代码开始,以内部代码结束。外部的需求和场景具有确定性,能够提供关于系统的重要的信息。从外部代码开始开发,能够在开发中聚焦于当前层级代码,可以在到达内部代码时明确关键的细节,可以从更好的视角看清楚职责分配。也就是先确定代码需求,后实现代码。
测试先行是指在由外而内开发的基础上,用测试来表达设计契约。设计契约就是代码入参、出参、期望行为的一系列信息。在领域驱动设计中,倡导首先确认测试用例,写出测试代码,明确对业务代码的预期,然后再写业务代码。实际上,在领域模型探索和发现中,已经明确了测试用例范围。测试先行能够在早期发现业务代码问题,把缺陷暴露在修改成本最低的时候。
总结
本文初步整理了领域驱动设计的核心概念、架构设计、事件风暴和开发思维:
- 核心概念:域、子域、限界上下文、领域实体、值对象、领域服务、领域事件、聚合、工厂、资源库;
- 架构设计:六边形架构;
- 事件风暴:探索和发现领域模型;
- 开发思维:由外而内和测试先行。
关于领域驱动设计,网络上概念比较多,代码比较少。需要在工作中实践和思考才能体会领域驱动设计的精髓。