购买
下载掌阅APP,畅读海量书库
立即打开
畅读海量书库
扫码下载掌阅APP

1.2
面向领域的设计方法

面向领域的设计方法是DDD的精髓。在本节中,我们将从战略设计和战术设计这两大维度出发,基于DDD的设计方法来实现系统的业务模型。事实上,面向领域的设计方法和业务模型的7个维度是一一对应的,让我们一起来看一下。

1.2.1 面向领域的战略设计

面向领域的战略设计包含DDD的一组核心概念,用于抽象业务的领域模型。图1-4展示了面向领域的战略设计与业务模型维度之间的映射关系。

图1-4 面向领域的战略设计与业务模型维度之间的映射关系

可以看到,这里引入了通用语言和限界上下文这两个核心概念。其中,DDD从业务角度通过通用语言来满足相关工作人员对业务模型进行描述的需要,确保开发人员与领域专家能够形成统一认识;通过限界上下文来实现对业务的拆分,过程中需考虑系统边界的划分方式及集成方式。

1.业务描述与通用语言

通用语言,也可以被称作统一协作语言,用来解决在实现业务模型时会遇到的一个非常重要的问题,即如何让团队所有人使用同一种语言来描述业务需求。在业务人员和技术人员的协作过程中,要使他们在意识形态和认知体系上达成一致并不是一件容易的事情,因为业务人员和技术人员都有其自身习惯的表达方式。引入通用语言的思路是面向领域和业务来统一团队成员对领域知识的一致认识,推动成员在后续系统设计和代码实现中使用领域词汇而不是技术词汇来命名业务对象。

通用语言的建立通常不是一步到位的,而是分层级持续演进的。例如,考虑一个用户健康监控和管理的业务场景,业务人员和开发人员经过初步沟通,得到了这样的通用语言:构建统一的健康监控功能,用户可以通过这一功能管理自己的健康信息。

在上述场景中,这句对原始需求的描述构成了系统最高层级的通用语言,后续从业务到技术的各个层次的通用语言都将由此展开。而开发人员与业务人员在进一步沟通之后,可以得到细化的通用语言,如下所示。

❑用户在申请健康检测时会生成一个健康检测单,同一个用户在上一个健康检测单没有完成之前无法申请新的检测单。

❑用户在申请健康检测单时需要提供自己的既往病史及目前的症状描述,然后系统需要根据用户的这些输入信息生成一个健康计划,健康计划被看作管理用户健康数据的一种执行媒介。

❑一个用户在同一时间只能有一份生效的健康计划,如果用户对系统自动生成的健康计划并不满意,可以重新申请生成健康计划。

❑健康计划的具体内容包括制定计划的医生、计划描述、执行周期、需要用户执行的健康任务列表等。

❑健康检测的结果表现为可以量化的健康积分,该健康积分会根据用户执行健康任务的完成情况不断更新。用户可以通过健康积分判断自己的健康状况。

上述对业务场景的描述构成了第二层级的通用语言,我们已经可以从这些描述中提取大量有助于开展系统设计工作的关键信息。当然,随着业务人员和技术人员采用同样的方式开展进一步沟通,我们将得到更多层级的通用语言,直到满足系统设计的目标为止。

2.业务拆分与限界上下文

在上一节中,我们讨论了系统架构的演进过程,这一过程给我们的启示就在于:将所有功能放在一起是不合理的,应该清晰划分业务系统的关注点,并通过功能拆分降低系统复杂度。

针对业务拆分,我们需要解决的第一个问题是如何找到拆分的切入点。针对这一问题,DDD给出了子域的概念。子域作为系统拆分的切入点,其产生往往取决于系统的特征和拆分的需求,例如这些需求属于核心功能、辅助性功能还是第三方功能等。

在业务领域被拆分成多个子域之后,接下来要解决的问题是如何对拆分后的功能进行组装。针对这一问题,基本思路就是系统集成,即在子域之间通过有效的集成方式确保拆分后的子业务功能能够整合到一起构成一个大的业务功能。与业务需求不同,系统集成需求虽然也包含在子域之中,但更多关注集成的策略和技术体系。在DDD中,限界上下文承接了这部分需求的实现。

限界上下文的概念比较难以理解,我们有必要对其详细讲解。首先,对于任何概念、属性和操作,每个领域模型在特定的业务边界之内具有特定的含义,这些含义只限于这个边界之内,也就是所谓的限界。一个限界上下文的简单示例如图1-5所示。

图1-5 限界上下文示例

如图1-5所示,限界上下文A和限界上下文B中都存在HealthPlan对象,但是限界上下文A中的HealthPlan是一个聚合对象,而限界上下文B中的HealthPlan则是一个实体对象,两者虽然命名相同,也代表着同一个逻辑概念,但在业务建模过程中却有本质区别。我们会在本章后文对聚合和实体等概念进一步展开讲解。

另外,限界上下文B中HealthPlan对象可能依赖于限界上下文C中的HealthTask对象,但HealthPlan和HealthTask显然属于不同的业务场景,这时候我们就会发现限界的划分在很大程度上影响着系统的设计和实现。

明确了子域和限界上下文,下一步就是把它们整合起来。每个子域都有其限界上下文,各个限界上下文可以根据需要进行有效整合,从而构成完整的领域模型。请注意,并非所有子域的定位和作用都一样。在下一章中,我们也会讨论子域的不同分类。

根据子域和限界上下文的概念,我们可以对系统进行拆分。系统拆分的策略可以灵活调整,而根据业务和通用语言进行系统拆分是面向领域的战略设计的前提,也是本书所推崇的方法。但在现实场景中,面对业务拆分的需求,开发人员往往会采用一些非面向领域的拆分方法,其中典型的方法如下。

1)根据技术架构进行拆分。这种方法违背了业务模型的设计思想,也不符合常见的业务系统所采用的业务架构驱动技术架构的原则。在业务梳理尚不完善、系统的战略设计尚不健全的情况下考虑技术架构和实现方法,往往会导致返工,并在对系统的不断修改中腐化架构。

2)根据开发任务进行拆分。根据开发任务拆分系统同样不是一个好主意。一方面,系统拆分实际还没有到具体开发资源和时间统筹的阶段,开发任务自然也无从谈起。另一方面,开发任务同样是面向技术的过程的产物,而不是面向业务。

3)根据团队职能进行拆分。团队按构建方式可以分为职能团队和特征团队。前者关注某一个特定职能,如常见的服务端、前端、数据库、UI团队等;而后者则代表一种跨职能的团队构建方式,团队中包括服务端、前端开发人员等各种角色。业务边界的划分及限界上下文的构建是一项跨职能的活动,如果团队组织架构具备跨职能特性,可以安排特定的团队负责特定的限界上下文,并统一管理该上下文对应的界限。

当我们实施领域驱动设计时,需要对上述拆分方法进行识别,避免采用不合理的拆分方法。

1.2.2 面向领域的战术设计

在DDD中,战术设计方面的内容非常多,包括表示领域模型对象的聚合、实体和值对象,用于抽象多个对象级别业务逻辑的领域服务,表示业务状态并实现交互解耦的领域事件,用于抽象数据持久化的资源库,以及用于提取业务外观的应用服务。图1-6展示了面向领域的战术设计与业务模型中对应维度之间的映射关系。

图1-6 面向领域的战术设计与业务模型维度之间的映射关系

1.业务对象与领域模型

关于对象这个词,在不同软件开发方法中有不同的含义。以最常见的“面向对象”为例,它表示任何事物都被认为一种对象。而设计和实现这些对象也可使用开发模式,例如,我们可以从数据的角度来规划对象的组织形式,并通过面向数据库的方式对这些数据对象进行设计和建模,这种开发模式通常被称为数据驱动模式。

显然,领域驱动设计的过程与数据驱动完全不同。在DDD中,我们关注的是领域模型对象而非数据本身。虽然以数据作为主要关注点的开发模式也能完成对系统的构建,但我们认为面向领域的模型对象才是通用语言的有效载体。究其原因,很多对象并不能简单地用它们的数据属性来定义,而需要通过一系列的标识和行为来定义。在DDD中,领域模型对象包括三大类型,即聚合、实体和值对象,如图1-7所示。

图1-7 领域模型对象的三大类型

与限界上下文被用来划分子域之间的业务边界一样,在领域模型对象中,我们也需要从软件复杂度的角度出发,明确对象之间的边界。软件设计的一大挑战就在于大多数系统中的业务逻辑存在十分复杂的关联关系,它们实际很少有清晰的边界。复杂的关系需要数量庞大的对象才能建立,而整个系统的开发和维护需要投入成本,这是架构腐化的根源之一。为此,DDD专门提出了聚合对象这一概念。聚合的核心思想在于简化对象之间的关联关系,一个聚合内部的所有对象只能通过聚合对象来访问,从而有效降低了对象之间的交互复杂度。

实体是聚合内部具有唯一标识的一种业务对象。与普通的数据对象一样,实体中也包含了一系列数据属性,我们可以采用一定的手段把数据对象转换成实体对象。但是,实体对象与数据对象之间的本质区别是:实体对象具有状态可变性和完整生命周期,我们可以通过改变实体的状态来执行业务逻辑;而数据对象只是数据的一种结构表现形式,本身没有任何状态和生命周期。有时候,我们把基于实体对象的建模方式称为充血模型,以便与基于数据对象的贫血模型相区别。

当我们只关心对象的数据属性时,该对象应被归为值对象。从这点上讲,值对象有点类似贫血模型对象。但值对象具备明确的约束条件,这点与实体对象不同。这些约束条件包括值对象是不变对象,值对象没有唯一标识,以及值对象通常不包含业务逻辑。

2.业务规则与领域服务

针对业务模型中的业务逻辑,我们可以把它们抽象成一组业务规则。业务规则从概念上讲通常不属于任何一个独立的对象,而是一组领域模型对象之间的交互和操作。显然,领域建模的基本表现范式是各种领域模型对象,但一些业务规则并不适合建模成独立的聚合或实体。这时候,DDD提供了领域服务的概念。当领域模型中某个重要操作无法由单个聚合或实体来完成时,应该为该模型添加一个独立的访问入口,这就是领域服务,如图1-8所示。

图1-8 领域服务与领域模型对象

从图1-8中可以看到,领域服务的构建涉及多个领域模型对象之间的交互和协作,这是单个领域模型对象所不能完成的操作。

3.业务状态与领域事件

现实中很多场景都可以抽象成事件,例如当某个操作发生时会发送一个消息,如果出现了某种情况则执行某个既定的业务操作等。本质上,这些事件代表的是业务状态的变化,如图1-9所示。

图1-9 业务状态变化与事件

与普通的领域模型对象不同,我们关注这些事件的发生时机,事件本身携带的状态变化信息,以及我们针对事件的响应方式。因此,事件是一种独立的建模对象,在DDD中被称为领域事件。领域事件就是把领域中所发生的活动建模成一系列离散事件。领域事件也是一种领域对象,是领域模型的重要组成部分。

4.业务数据与资源库

让我们回到对业务数据的讨论。任何系统都要对业务数据进行统一的管理和维护,开发人员会把数据保存到各种关系型数据库或NoSQL等数据持久化媒介中,这是数据驱动模式的基本开发过程。而在领域驱动的开发模式中,我们认为系统中应该存在一个专门针对数据访问的入口,通过这个入口可以对所有的领域模型对象进行遍历。无论是对聚合、实体还是对值对象,我们都应该创建另一种对象来充当这些对象的提供者。在DDD中,资源库实际上就充当了领域模型对象的提供者。资源库的定位和交互方式如图1-10所示。

图1-10 资源库的定位和交互方式

简单讲,资源库作为对象的提供者,能够实现对象的持久化,但这种持久化操作是技术无关的,即领域模型不需要关注通过何种技术来获取和存储领域模型对象,只需要明确对象的来源是资源库。资源库为开发人员屏蔽了数据访问的技术复杂性。

5.业务外观与应用服务

最后,让我们讨论业务外观的概念。任何一个系统都会不可避免地存在用于用户交互的用户界面,也可能存在与外部系统对接的集成需求。无论是用户界面还是系统集成,都不是领域驱动设计的重点。但是,针对用户界面,我们需要明确两个基本问题:如何将领域对象渲染到用户界面中,以及如何将用户操作反映到领域模型中。针对系统集成也是同样。我们把这部分工作统称为业务外观。

在实现业务外观与领域模型之间的解耦时,我们可以使用的设计模式也很多,如数据传输对象(Data Transfer Object,DTO)模式和外观模式。

在DTO模式中,传输对象是一种简单的POJO(Plain Ordinary Java Object,简单Java对象),只有设置和获取属性的方法,而客户端则可以通过请求将传输对象发送到领域模型中。

对于外观模式,其设计意图在于为系统中的一组接口提供一个一致的界面,从而使得这一系统更加容易使用。在实现上,与系统发生直接耦合的不是用户界面,而是外观类。在分层架构中,可以使用这一模式定义系统中每一层的入口。外观模式的结构示意图如图1-11所示。

图1-11 外观模式结构示意图

在领域驱动设计中,我们将使用应用服务实现类似DTO模式和外观模式的功能。关于应用服务以及前面提到的各种领域对象,在下一章中都会有更深入的探讨。

最后,我们可以梳理出面向领域的设计方法与业务模型各个维度之间的对应关系,如图1-12所示。

图1-12 DDD与业务模型之间的对应关系

请注意,图1-12中DDD的各个组件并不是位于同一层次的,各个限界上下文都应该包括战术设计的所有技术组件,如图1-13所示。

图1-13 限界上下文中包含各个战术设计组件示意图 8vgQos7oq8OC6tKxxw1Nrh8B5uz4rle1Yu/UVDSpZgvDIqrzxTeVA8GsJIDA0bcd

点击中间区域
呼出菜单
上一章
目录
下一章
×