领域驱动设计篇
一句话概述:领域驱动设计(DDD) 是一种从业务出发的设计方法,其核心是通用语言(业务与开发共用一套语言)和模型驱动设计,通过战略设计划分模块、战术设计构建模型。
知识图谱
图注:DDD 三大核心——通用语言与模型驱动设计(根基)、战略设计(宏观划分)、战术设计(微观建模)。
7.1 领域驱动设计概述
核心定义
领域驱动设计(Domain-Driven Design,DDD) 是一种从业务出发的设计方法,通过使用通用语言让业务人员参与设计,拉近业务人员与开发人员之间的距离,同时提供一套标准的建模方法,帮助团队识别业务模型,避免开发者犯下低级错误。
设计方法的演进
在软件开发实践中,设计方法经历了从"围绕数据做设计"到"领域驱动设计"的演进。
围绕数据做设计的传统做法:许多开发者首先会设计数据库,因为他们认为程序就是数据加函数。数据需要存到数据库里,剩下的就是根据需要对数据库表进行增删改查。这种思路本质上是一种结构化编程的思路。后来有人采用面向对象的思路,先来找实体(对象),让这些实体拥有一些能力,最终这些对象还是要写到数据库里,同样提供增删改查的能力。
这两种做法本质上没有太大区别,都是围绕着数据在做文章。在业务需求不复杂的年代,围绕数据做文章的做法还能满足开发的要求。但随着软件日益深入到人们日常工作和生活中,软件变得越来越复杂,这种做法就越发显得难以适应了。
图注:从"围绕数据做设计"到"领域驱动设计"的演进——DDD 的核心价值在于从业务出发、标准化建模、系统化划分。
DDD 的诞生与发展
领域驱动设计作为一个新的设计方法正式登上历史舞台,是从 Eric Evans 的著作《领域驱动设计》正式出版开始的。这种设计方法成功地解决了业务软件开发中遇到的大部分问题——但它不是万能药,对大部分人面对的场景而言,它都能够有效地应对。
理论上,这种设计方法如此优秀,应该很快流行起来才对。然而实际情况是,很多程序员都不知道 DDD。一个重要的原因就是 Eric 的这本书表述得不够清晰。要想从中深入理解,读者需要比较懂 DDD,但是大多数人并不懂,这就形成了认知障碍。所以,DDD 在很长一段时间都被埋没了。
后来,随着微服务的兴起,人们越发认识到,微服务的难度并不在于将一个系统拆分成若干的服务,而在于如何有效地划分微服务。这个时候,人们发现,DDD 才是最恰当的指引。
DDD 的根基
学习 DDD,需要从理解 DDD 的根基入手:通用语言(Ubiquitous Language) 和模型驱动设计(Model-Driven Design)。领域驱动设计的过程,就是建立起通用语言和识别模型的过程。
7.2 通用语言
核心定义
通用语言(Ubiquitous Language) 是在业务人员和开发人员之间建立的一套共有语言。在从前的设计方法中,业务人员总是将问题描述移交,让开发人员去解决。业务人员讨论的都是业务名词,例如产品、订单等等,而开发人员的讨论集中于技术细节,比如线程、存储等等。二者除了最基础的几个概念之外,其他的内容基本无法沟通。一道人为鸿沟就在开发人员和业务人员之间形成了。
软件设计是要在问题和解决方案之间架设一座桥梁,好的设计要更接近问题。开发人员对解决方案一端再熟悉不过,但是对业务一端理解通常不够充分。而通用语言所做的事情,就是把开发人员的思考起点拉到业务上,也就是从问题出发,这就在一定程度上填平了那道人为的鸿沟。
通用语言的内容
通用语言包含业务中的概念和操作。以电商平台为例:
- 概念:产品、订单等
- 操作:产品的上架、下架、修改产品信息;订单的下单、撤单、修改订单等
在业务人员看来,这里说的都是自己擅长的事情,自己就可以有更多的发言权。在开发人员的视角,概念就是一个一个的类,操作就是一个一个的方法,也很好理解。所以,有一套通用语言,双方均能接受。
构建通用语言的方法
白板讨论法
基础方法为让业务人员和开发人员一起,找一块白板,把各种概念都写在上面。然后,双方重新进行分类整理。这里面的重点是,让业务人员和开发人员在一起。如果只让一方出现,结果又会是原来的样子,因为无法判断这里面的语言对方是否听得懂。
这种方法很简单,但通常都不够系统,会存在各种遗漏。
事件风暴
有人探索出一种更正式的实践:事件风暴(Event Storming)。事件风暴是一个工作坊,基本方法是找一面很宽的墙,上面铺上大白纸,然后用便利贴把识别出来的概念贴在上面。当然,前提依然是让业务人员和技术人员都参与其中。
这个实践之所以叫作事件风暴,因为它的关注点在于领域事件。领域事件是用来记录业务过程中发生过的重要事情。例如,作为电商平台的工作人员,需要确认产品是不是已经上架了,这个领域事件就是"产品已上架";作为消费者,会关心订单是不是下成功了,这个领域事件就是"订单已下"。
执行某项操作之后,都会关心该操作产生的结果,所以,领域事件采用的描述方式都是过去式,比如:OrderPlaced。
图注:事件风暴的三步法——识别领域事件、识别命令、识别聚合。
事件风暴工作坊主要分成三步:
-
第一步:识别领域事件——系统会产生哪些关键业务结果。有了领域事件,下面一个问题是,这些事件是如何产生的,它必然会是某个动作的结果。
-
第二步:找出命令——找出引发领域事件的动作(命令)。例如:产品已上架是由"产品上架"这个动作引发的,而订单已下就是由"下单"这个命令引发的。
-
第三步:找出实体或聚合——找出与事件和命令相关的实体或聚合。例如,产品上架就需要有个产品(Product),下单就需要有订单(Order)。
通常,在工作坊过程中,为了增强趣味性和清晰性,不同的概念会用不同颜色的便利贴标识出来,例如,领域事件用橙色、命令用蓝色、实体/聚合用黄色等等。
四色建模
用不同的颜色建模,事件风暴并非唯一方法。Peter Coad 曾提出过一种四色建模的方法:
- 粉色表示时标性对象(moment-interval)
- 黄色表示角色(role)
- 蓝色表示描述(description)
- 绿色表示人、地点、物(party/place/thing)
他还写了一本《彩色UML建模》(Java Modeling in Color with UML)介绍这种方法。
电商平台通用语言示例
| 通用语言术语 | 业务理解 | 开发理解 |
|---|---|---|
| 产品(Product) | 销售的商品 | Product 类 |
| 订单(Order) | 用户的购买记录 | Order 类 |
| 上架(List) | 产品开始销售 | product.list() 方法 |
| 下架(Delist) | 产品停止销售 | product.delist() 方法 |
| 下单(Place Order) | 用户购买商品 | order.place() 方法 |
| 撤单(Cancel Order) | 用户取消订单 | order.cancel() 方法 |
核心要点
- 通用语言将开发人员的思考起点拉到业务上——从问题出发
- 业务人员说业务名词(产品、订单),开发人员说技术(线程、存储)→ 通用语言填平鸿沟
- 通用语言中的概念 → 类;操作 → 方法
- 构建通用语言的方法:白板讨论 → 事件风暴(更系统化的实践)
- 关键:业务人员和开发人员必须在一起
7.3 战略设计
核心定义
战略设计是高层设计,是指将系统拆分成不同的领域。领域驱动设计,核心的概念就是领域,也就是说,它给了我们一个拆分系统的新视角:按业务领域拆分。
战略设计中的概念可以分为两部分:做业务的划分和落地成解决方案。一部分是为了将不同的业务区分开来,也就是要将识别出来的业务概念做一个划分;另一部分则是将划分出来的业务落实到真实的解决方案中。
业务概念的划分
领域与子域
软件开发是在解决问题,要解决的问题就是领域(Domain),它对应着要解决的问题。正如一直强调的,软件开发是解决问题,而解决问题要分而治之。所谓分而治之,就是要把问题分解了,对应到领域驱动设计中,就是要把一个大领域分解成若干的小领域,而这个分解出来的小领域就是子域(Subdomain)。
领域驱动设计首先要建立起一套通用语言,这样就拥有了各种各样的词汇,它们对应着模型。接下来,就要给这些词汇做个分类,而分类就是要把它们划分到不同的子域中去。这里面的关键在于,要找出不同的关注点——即分离关注点。
以项目管理软件为例,需要有用户、有项目、有团队,不同的人还要扮演不同的角色。第一步,至少可以先把身份管理和项目管理这两件事分开,因为它们的关注点是不同的。身份管理关注的是用户的身份信息,诸如用户名密码之类的,而项目管理关注的重点是项目和团队之类的。所以,这里有了两个子域:身份管理和项目管理。
划分出不同的子域还是比较容易出问题的,因为有一些概念并不容易区分。例如,用户应该怎么划分呢?放在身份管理是合适的,但项目管理也要用到用户。值得庆幸的是,已经学习了单一职责原则,它给出了一个重要的思考维度:变化从何而来。不同角色的人会关注不同的变化,所以,虽然用的词都是"用户",但所表达的语义却是不同的,最好将这些不同的含义分开,也就是将不同的角色分开。例如,在身份管理中,它是"用户",而在项目管理中,它就成了"项目成员"。所以,划分子域实际上就是在把不同的概念区分开来,让它们各归其位。
核心域、支撑域、通用域
对于一个真实项目而言,划分出来的子域可能会有很多,但并非每个子域都一样重要。所以,还要把划分出来的子域再做一下区分,分成核心域(Core Domain)、支撑域(Supporting Subdomain)和通用域(Generic Subdomain)。
图注:子域的三种类型——核心域(全力投入)、支撑域(次级投入)、通用域(可买服务)。
核心域是整个系统最重要的部分,是整个业务得以成功的关键。Eric Evans 曾提出过几个问题,帮助识别核心域:
- 为什么这个系统值得写?
- 为什么不直接买一个?
- 为什么不外包?
如果对这几个问题的回答能够帮助找到这个系统非写不可的理由,那它就是核心域。
支撑域是指有一些子域不是核心竞争力,但却是系统不得不做的功能,市场上也找不到一个现成的方案。例如,要实现排行榜功能,可能根据各种信息做排名,市场上缺乏针对此类需求的现成方案,但对自身系统而言又是重要的扩展功能,它就是一个支撑域。
通用域是指行业里通常都是这么做的,即便不自己做,也并不影响业务运行。例如,很多 App 要给用户发通知,这样的功能完全可以采用外部服务替代,丝毫不影响业务运行。它就是一个通用域。
区分不同的子域,关键的原因在于可以决定不同的投资策略:核心域要全力投入,支撑域次之,通用域可采用外部服务替代。
业务概念的落地
限界上下文
通过划分子域,区分核心域、支撑域和通用域,DDD 在问题层面的概念已经阐述清楚了。接下来,就要进入到解决方案层面了。
现在有了切分出来的子域,怎样去落实到代码上呢?首先要解决的就是这些子域如何组织的问题,是将所有子域置于单一应用程序中,还是为每个子域构建独立应用,抑或是采用混合部署方式。这就引出了领域驱动设计中的一个重要概念:限界上下文(Bounded Context)。
限界上下文,即形成了一个边界,一个限定了通用语言自由使用的边界,一旦出界,含义便无法保证。例如,同样是说"订单",如果不加限制,很难区分它是用在哪种场景之下。而一旦定义了限界上下文,那交易上下文的"订单"和物流上下文的"订单"肯定是不同的。原因就在于,订单这个说法,在不同的边界内,含义是不一样的。
图注:电商系统的限界上下文——同一个"产品"在销售和库存上下文中含义不同。限界上下文之间通过明确的关系进行交互。
注意,子域和限界上下文不一定是一一对应的,可能在一个限界上下文中包含了多个子域,也可能在一个子域横跨了多个限界上下文。
限界上下文是在解决方案层面的,所以,很自然地,就可以把限界上下文看作是一个独立的系统。很多团队做微服务的时候,最纠结的问题就是如何划分服务边界,而限界上下文的出现与微服务的设计理念高度契合,每个限界上下文都可以成为一个独立的服务。
限界上下文的重点在于,它是完全独立的,不会为了完成一个业务需求要调用其他服务执行大量操作,这正是许多微服务架构面临的问题,例如,一个业务功能要调用很多其他系统的功能。
上下文映射图
有了对限界上下文的理解,就可以把整个业务分解到不同的限界上下文中。但是,尽管拆分了系统,它们终究还是一个系统,不可避免地要进行交互。例如,一个用户下了订单,这是在订单上下文中完成的。那接下来,用户要去支付,这是在支付上下文中完成的。肯定要通过某种途径让订单上下文的一些信息发送到支付上下文里。
所以,就要有一种描述方式,将不同限界上下文之间交互的方式描述出来,这就是上下文映射图(Context Map)。DDD 提供了一些描述这种交互的方式:
| 关系类型 | 说明 |
|---|---|
| 合作关系(Partnership) | 两个上下文紧密合作 |
| 共享内核(Shared Kernel) | 共享部分模型 |
| 客户-供应商(Customer-Supplier) | 上下游关系 |
| 跟随者(Conformist) | 直接依赖上游模型 |
| 防腐层(Anticorruption Layer) | 隔离外部模型影响 |
| 开放主机服务(Open Host Service) | 提供标准协议 |
| 发布语言(Published Language) | 事件/消息格式 |
| 各行其道(Separate Ways) | 完全独立 |
| 大泥球(Big Ball of Mud) | 混乱无序(应规避) |
之所以有这么多不同的交互方式,旨在明确限界上下文之间的交互模式。当然,如此多的交互方式无需全部记忆,有些甚至是需要规避的,例如大泥球。如果必须记住一种交互方式,那就是防腐层(Anticorruption Layer)。
防腐层是最具防御性的一种关系,简而言之,就是指要在外部模型和内部模型之间建立起一个翻译层,将外部模型转化为内部模型。但凡有可能,就要建立防腐层,将外部模型完全隔离开。
当确定了不同的限界上下文之间采用哪种交互方式之后,不同的交互方式就可以落地为不同的协议。当前主流协议包括 REST API、RPC 或消息队列,可根据实际情况进行选择。
在定义好不同的限界上下文,将它们之间的交互呈现出来之后,就得到了一张上下文映射图。上下文映射图可以帮助理解系统的各个部分之间是怎样进行交互的,帮助建立一个全局性的认知,这正是许多团队所缺乏的。
核心要点
- 战略设计是高层设计,将系统拆分成不同的领域
- 子域划分:核心域(全力投入)、支撑域(次级投入)、通用域(可买服务)
- 限界上下文:限定了通用语言自由使用的边界,可以成为独立的系统
- 上下文映射图:定义不同上下文之间的交互方式
- 如果只能记住一种交互方式,那就是防腐层
7.4 战术设计
核心定义
战术设计是低层设计,也就是如何具体地组织不同的业务模型。在这个层次上,DDD 提供了一些标准的做法供参考。
战术设计同样包含了很多概念,例如实体、值对象、聚合、领域服务、应用服务等等。可将战术设计类比为故事构建:先构建好故事的背景,然后设定不同的角色,接下来创建角色之间的关系,最后安排角色之间的互动,形成完整的故事。
对于战术设计而言,故事的背景就是面对的领域问题,剩下的就是在这个故事背景下,要找出不同的角色,找出角色之间的关系,建立对象间的交互关系,从而完成战术设计。
角色:实体与值对象
首要任务就是设计角色,在战术设计中,角色就是各种名词。名词识别是面向对象设计的常见起点。有一些设计方法会先建立数据库表,这种做法本质上也是从识别名词入手的。在战术设计中,要识别的名词包括了实体和值对象。
实体(Entity)
实体(Entity)指的是能够通过唯一标识符标识出来的对象。
在业务处理中,有一类对象会有一定的生命周期。以电商平台上的订单为例,它会在一次交易的过程中存在,而在它的生命周期中,它的一些属性可能会有变化。例如,订单的状态刚开始是下单完成,然后在支付之后,变成了已支付,在发货之后就变成了已发货。但是这个订单始终都是这个订单,因为这个订单有唯一的标识符,也就是订单号。订单号作为它的标识符能将它标识出来。可以通过订单号查询它的状态,可以修改订单的一些信息,例如配送的地址。像这种通过唯一标识符标识出来的对象,就是实体。
开发者对实体概念普遍熟悉,因为在各种设计方法中,都有相应的方法识别实体。甚至可简化理解为数据库里存储的对象,虽然这种理解并不完全正确。
值对象(Value Object)
值对象(Value Object)表示一个值。 例如,订单地址,它是由省、市、区和具体住址组成。它与实体的差别在于,它没有标识符。之所以它叫值对象,是因为其行为特征类似于值。值对象可能会有很多属性,而要想判断值对象是否相等,就要判断这些属性是否相等。对于两个订单地址来说,只有省、市、区和具体住址等多个属性都相同,才认为它们是同一个地址。
实体的属性是可以变的,只要标识符不变,它就还是那个实体。但是,值对象的属性却不能变,一旦变了,它就不再是那个对象,所以,会把值对象设置成一个不变的对象。在函数式编程的不变性中,介绍了不变性的诸多好处,这里也完全适用于值对象。
图注:实体与值对象的区别——实体有唯一标识符、属性可变;值对象无标识符、属性不可变。
为什么要区分实体和值对象
将对象分为实体和值对象,主要是为了分离出值对象,即将可变对象和不可变对象区分开。在传统的做法中,实体识别是常规步骤,而在不同的模型中,值对象的识别通常是欠缺的考虑。
一方面,会将一些值对象当作实体,但实际上这类对象并不需要一个标识符;另一方面,也是更重要的,就是很多值对象并没有识别出来。例如,很多人会用一个字符串表示电话号码,会用一个 double 类型表示价格,而这些数据实际上都应该是一个值对象。
之所以说这里缺少了对象,原因就在于基本数据类型缺乏行为封装。在 DDD 的对象设计中,对象应当具有行为。例如,价格实际上有精度的限制,计算时有自己的计算规则。如果不使用一个类将它封装起来,这种行为就将分散在代码的各处,变得难以维护。
在讨论面向对象的封装时前文已述及,只有数据的对象是封装没做好的结果,一个好的封装应该是基于行为的。在 DDD 的相关讨论中,经常有人批评所谓的"贫血模型",指的正是这种缺乏行为抽象的模型。
关系:聚合与聚合根
选定了角色之后,接下来就该考虑它们的关系了。
在传统的开发中,经常会遇到一个难题。例如,如果有一个订单,它有自己对应的订单项。由此引发的问题是,获取订单时,是否应当同时获取订单项?若一并获取可能导致数据量过大;若按需获取则逐条查询会影响性能。这就是典型的一对多问题,只不过,在其他场景中,主角就变成了各种其他的对象。
这也是一种用技术解决业务问题的典型思路。该困境的产生原因在于,考虑问题的出发点是技术。如果把考虑问题的出发点放到业务上呢?
战术设计就给出了这样一个思考的维度:聚合。
聚合(Aggregate)
聚合(Aggregate)就是多个实体或值对象的组合,这些对象之间具有同生共死的关系。 例如,一个订单里有很多个订单项,如果这个订单作废了,这些订单项也就没用了。所以,通常将订单和订单项视为一个整体单元,订单和订单项就是一个聚合。
学习 DDD 时,相关资料指出,聚合要保证事务(Transaction)一致性。简而言之,就是要更新就一起更新,要删除就一起删除。只要理解了它们是一个整体,就不难理解为什么这些对象要一起操作了。
聚合根(Aggregate Root)
一个聚合里可以包含很多个对象,每个对象里还可以继续包含其它的对象,就像一棵大树一层层展开。但重点是,这是一棵树,所以,它只能有一个树根,这个根就是聚合根。
聚合根(Aggregate Root),是从外部访问这个聚合的入口。 以订单和订单项为例,在订单和订单项组成的这个聚合里,订单就是聚合根。若需访问聚合内部的对象,须通过订单(聚合根)进行访问,即通过订单号找到订单,然后将相关的订单项一并取出。
图注:订单聚合示例——订单是聚合根,订单项、配送地址、订单金额都从属于订单聚合。外部只能通过订单(聚合根)访问聚合内部的对象。
实际上,可以将所有的对象都视为一种聚合。只不过,有一些聚合根下还有其他的对象,有一些没有而已。这样一来,就有了一个统一的视角看待所有的对象了。所以,也可以用统一的标准要求聚合,例如,聚合不能设计得太大。这体现了单一职责原则在聚合设计中的应用。
聚合之间的关系
如果不同的聚合之间有关系怎么办?例如,要在订单项里知道到底买了哪个产品,这个时候,在订单项里保存的不是完整的产品信息,而是产品 ID。实体是有唯一标识符的。如果需要,就可以根据产品 ID 找出产品信息。
有了对于聚合的理解,做设计的时候,就要识别出哪些对象可以组成聚合。所以,一对多问题也就不再是问题了:是聚合的,可以一次都拿出来;不是聚合的,就靠标识符按需提取。当纠结于技术时,应当首先反思问题定义是否正确。
互动:领域服务、领域事件、工厂、仓库、应用服务
现在有角色了,也确定关系了。接下来,就要安排互动了,也就是说,要把业务流程完整地阐述清楚。
回顾事件风暴过程,在其中识别出了事件和动作,而业务流程的完整阐述正是基于这些事件和动作。因为有了各种动作,各种角色才能够生动地活跃起来,整个业务流程才得以展开。
领域事件(Domain Event)
动作的结果会产生各种事件,也就是领域事件。领域事件相当于记录了业务过程中最重要的事情。相对于 DDD 中的其他概念,领域事件引入 DDD 体系的时间相对较晚,但因其具有重要价值,迅速成为 DDD 中不可或缺的一个重要概念。
因为领域事件是一条很好的主线,可以帮助梳理出业务上的变化。同时,在分布式系统广泛应用的当下,领域事件可以帮助系统达成最终一致的状态。
领域服务(Domain Service)
各种动作又是什么呢?那就是动词。在战术设计中,领域服务(Domain Service) 就是动词。只不过,它操作的目标是领域对象,更准确地说,它操作的是聚合根。
动词,是在学习面向对象过程中最为缺少的一个环节,多数教材侧重于如何识别名词。在实际编码中,会大量地使用像 Handler、Service 之类的名称,它们本质上就是动词。
按照前面的说法,动作不应该在实体或值对象上吗?诚然如此,能放到这些对象上的动作固然可行,但是,总会有一些动作不适合放在这些对象上。例如,要在两个账户之间转账,这个操作牵涉到两个账户,肯定不能放到一个实体类中。这样的动作就可以放到领域服务中。
工厂(Factory)
还有一类动作也比较特殊,就是创建对象的动作。显然,这个时候还没有对象,所以,这一类的动作也要放在领域服务上。这种动作对应的就是工厂(Factory)。工厂即设计模式中常提到的工厂模式,有了设计模式的基础之后,理解起来就容易多了。
需要注意的是,由于聚合的存在,聚合里的各种子对象都要从聚合根创建出来,以便保证二者之间的关联。例如,订单项的产生应当从订单上的订单项工厂方法创建出来。而聚合根本身的产生,就可以由领域服务来扮演工厂的角色。
仓库(Repository)
对于这些领域对象,无论是创建,还是修改,都需要有一个地方把变更的结果保存下来,而承担这个职责的就是仓库(Repository)。可将其理解为持久化抽象(当然,在不同的项目中,具体的实现还是会有些差别的)。
实际上,开发者熟悉的 CRUD 操作,可以对应成一个个的领域服务。如果用战术设计的方式来表示,应当是这样:
- 创建(Create):从工厂中创建出一个对象,然后保存到仓库中
- 查询(Read):通过仓库进行查询
- 修改(Update):通过仓库找到要修改的对象,修改之后,存回到仓库中
- 删除(Delete):通过仓库找到要删除的对象,然后在仓库中删除
当然,这种简单的映射并不理想,没有充分体现业务含义,此处仅用于帮助在已有知识和新知识之间架设桥梁。
应用服务(Application Service)
当把领域服务构建起来之后,核心的业务逻辑基本就成型了。但要构建一个完整的系统,还会有一些辅助性功能,例如,用户要修改一个订单,但首先要保证这个订单是他的。在 DDD 中,承载这些内容的就是应用服务。
应用服务可以扮演协调者的角色,协调不同的领域对象、领域服务等完成客户端所要求的各种业务动作,所以,亦被称为"工作流服务"。一般来说,一些与业务逻辑无关的功能都会放到应用服务中完成,例如,监控、身份认证等等。
应用服务和领域服务之间最大的区别在于,领域服务包含业务逻辑,而应用服务不包含。 关于业务逻辑的界定,需结合具体的项目来确定。
图注:DDD 战术设计中的核心模型要素——聚合根管理一致性边界,实体有唯一标识,值对象不可变,领域服务承载跨实体逻辑,仓库提供持久化抽象,应用服务协调业务流程。
核心要点
- 实体(Entity):有唯一标识符的对象,生命周期内有连续性
- 值对象(Value Object):无标识符,由属性定义,不可变
- 聚合(Aggregate):多个实体或值对象的组合,同生共死
- 聚合根(Aggregate Root):从外部访问聚合的入口
- 领域服务(Domain Service):不属于任何实体或值对象的业务逻辑
- 领域事件(Domain Event):业务过程中发生的有意义的事件
- 工厂(Factory):创建对象的职责
- 仓库(Repository):持久化的抽象接口
- 应用服务(Application Service):协调者,不包含业务逻辑
本篇核心概念关系
图注:DDD 核心概念关系图——问题空间(领域、子域)映射到解决方案空间(限界上下文),在限界上下文内部进行战术设计(实体、值对象、聚合等)。
与其他知识点的关联
- 01-设计认知篇 → 模型与规范:DDD 的战术设计就是系统化地构建模型
- 04-编程范式篇 → 面向对象:DDD 充分运用了面向对象的封装、继承、多态
- 05-设计原则篇 → 单一职责原则:限界上下文的划分体现了 SRP 在系统级别的应用
- 05-设计原则篇 → 依赖倒置原则:仓库接口在领域层定义,实现在基础设施层——DIP 的典型应用
- 06-设计模式篇:DDD 中的工厂、仓库等概念借鉴了设计模式
- 08-设计实战篇:数据采集平台的设计实践
延伸阅读
- Eric Evans《领域驱动设计》—— DDD 的开山之作
- Vaughn Vernon《领域驱动设计精粹》—— 快速入门 DDD
- Vaughn Vernon《实现领域驱动设计》—— 更细致的 DDD 实践指南