在理解领域模型之前,我们先思考一下软件开发的本质是什么。从本质上来说,软件开发过程就是问题空间
到解决方案空间
的一个映射转化,如图1所示。
在问题空间中,我们主要是找出某个业务面临的挑战及其相关需求场景用例分析;而在解决方案空间中,则通过具体的技术工具手段来进行设计实现。
就软件系统来说,“问题空间”就是系统要解决的“领域问题”
。因此,也可以简单理解为一个领域就对应一个问题空间,是一个特定范围边界内的业务需求的总和。
“领域模型”
就是“解决方案空间”,是针对特定领域里的关键事物及其关系的可视化表现,是为了准确定义需要解决问题而构造的抽象模型,是业务功能场景在软件系统里的映射转化,其目标是为软件系统构建统一的认知。
例如,请假系统解决的是人力工时的问题,属于人力资源领域,对口的是HR部门;费用报销系统解决的是员工和公司之间的财务问题,属于财务领域,对口的是财务部门;电商平台解决的是网上购物问题,属于电商领域。可以看出,每个软件系统本质上都解决了特定的问题,属于某一个特定领域,实现了同样的核心业务功能来解决该领域中核心的业务需求。
DDD是Eric Evans在2003年出版的《领域驱动设计:软件核心复杂性应对之道》(Domain-Driven Design
: Tackling Complexity in the Heart of Software)一书中提出的具有划时代意义的重要概念,是指通过统一语言、业务抽象、领域划分和领域建模等一系列手段来控制软件复杂度的方法论.
DDD的革命性在于领域驱动设计是面向对象分析的方法论
,它可以利用面向对象的特性(封装、多态)有效地化解复杂性,而传统J2EE或Spring+Hibernate等事务性编程模型只关心数据
。这些数据对象除了简单的setter/getter方法外,不包含任何业务逻辑,业务逻辑都是以过程式的代码写在Service中。这种方式极易上手,但随着业务的发展,系统也很容易变得混乱复杂。
领域驱动设计关心的是业务中的领域划分
(战略设计)和领域建模
(战术设计),其开发过程不再以数据模型为起点,而是以领域模型为出发点,研发过程如图2所示。领域模型对应的是业务实体,在程序中主要表现为类、聚合根和值对象,它更加关注业务语义的显性化表达,而不是数据的存储和数据之间的关系
。
统一语言(Ubiquitous Language)的主要思想是让应用能和业务相匹配
,这是通过在业务与代码中的技术之间采用共同的语言达成的。
业务语言起源于公司的业务侧,业务侧拥有需要实现的概念。业务语言中的术语由公司的的业务侧和技术侧通过协商来定义(意味着业务侧也不能总是选到最好的命名),目标是创造可以被业务、技术和代码自身无歧义使用的共同术语,即统一语言
。代码、类、方法、属性和模块的命名必须和统一语言相匹配,必要的时候需要对代码进行重构!
试想,在PRD文档、设计文档、代码以及团队日常交流中,如果有一套领域术语是统一无歧义的,是否会极大地提升沟通和工作效率?在日常工作中,因为概念理解不一致,或者语言表达上的问题,导致沟通效率低,甚至发生误解的情况实在太多了。所以,明确概念、形成统一语言至关重要。
DDD的核心是领域模型,这一方法论可以通俗地理解为先找到业务中的领域模型,以领域模型为中心,驱动项目开发。
领域模型的设计精髓在于``面向对象分析、对事物的抽象能力,一个领域驱动架构师必然是一个面向对象分析的大师。 DDD鼓励我们接触到需求后第一步就是考虑领域模型,而不是将其切割成数据和行为,然后用数据库实现数据,用服务实现行为,最后造成需求的首尾分离。 DDD会让你首先考虑业务语言,而不是数据。DDD强调业务抽象和面向对象编程,而不是过程式业务逻辑实现。
重点不同,导致编程世界观不同```。
建模本质上是一种抽象。抽象就是归类,其目的是减轻认知的负担,避免重复的思考和工作,提升人的计算能力。
统一语言也好,面向对象也好,最终的目都是为代码的可读性和可维护性服务。统一语言使得我们的核心领域概念可以无损地在代码中呈现,从而提升代码的可理解性。
面向对象也是让代码尽量体现领域实体和实体之间的关系原貌,所以目的也是业务语义被显性化地表达,显性化的结果是代码更容易被理解和维护,殊途同归,一切都是为了控制复杂度。在软件的世界里,任何的方法论如果最终不能落在“减少代码复杂度”这个焦点上,那么都是有待商榷的。
代码复杂度是由业务复杂度和技术复杂度共同组成的。实践DDD还有一个好处,是让我们有机会分离核心业务逻辑和技术细节,让两个维度的复杂度有机会被解开和分治。如图3所示,核心业务逻辑是整个应用的核心,最好只是简单Java类(Plan Old Java Object,POJO)。也就是说,核心业务逻辑对技术细节没有任何依赖,依赖都是由外向内的,即使有由内向外的依赖,也应该通过依赖倒置来反转依赖的方向。通过这样的划分,Entities只要安安心心地处理业务逻辑就好,业务逻辑越复杂,这样划分带来的好处越明显。
毫不夸张地说,我们的软件系统就是对现实世界的真实模拟。如图4所示,现实世界中的事物在软件世界中可以被模拟成一个对象:该事物在现实世界中被赋予什么职责,在软件世界中就被赋予什么职责;在现实世界中拥有什么特性,在软件世界中就拥有什么属性;在现实世界中拥有什么行为,在软件世界中就拥有什么函数;在现实世界中与哪些事物存在怎样的关系,在软件世界中就应当与它们发生怎样的关联。这正是面向对象编程的核心思想,也是DDD中寻找领域实体的核心思想。
聚合根(Aggregate Root)是DDD中的一个概念,是一种更大范围的封装,会把一组有相同生命周期、在业务上不可分割的实体和值对象放在一起,只有根实体可以对外暴露引用
,这也是一种内聚性的表现。
仍以银行转账的例子来说明,如图5所示,账号(Account)是客户信息(CustomerInfo)Entity和值对象(Address)的聚合根,交易(Tansaction)是流水(Journal)的聚合根,流水是因为交易才产生的,具有相同的生命周期。
有些领域中的动作是一些动词,看上去并不属于任何对象。它们代表了领域中的一个重要的行为,所以不能忽略它们或者简单地把它们合并到某个实体或者值对象中。当这样的行为从领域中被识别出来时,推荐的实践方式是将它声明成一个服务
。这样的对象不再拥有内置的状态,其作用仅仅是为领域提供相应的功能。Service往往是以一个活动来命名,而不是Entity来命名。
例如在银行转账的例子中,转账(transfer)这个行为是一个非常重要的领域概念,但是它发生在两个账号之间,归属于账号Entity并不合适,因为一个账号Entity没有必要去关联它需要转账的账号Entity。在这种情况下,使用MoneyTransferDomainService就比较合适了。识别领域服务,主要看它是否满足以下3个特征。
(1)服务执行的操作代表了一个领域概念,这个领域概念无法自然地隶属于一个实体或者值对象。
(2)被执行的操作涉及领域中的其他对象。
(3)操作是无状态的。
领域事件(Domain Event)是在一个特定领域由一个用户动作触发的,是发生在过去的行为产生的事件,而这个事件是系统中的其他部分或者关联系统感兴趣的。
为什么领域事件如此重要?因为在分布式环境下,很少有业务系统是单体的(Monolithic),消息作为分布式系统间耦合度最低、最健壮、最容易扩展的一种通信机制,是我们实现分布式系统互通的重要手段。关于领域事件,我们需要注意两点,分别是事件命名和事件内容。
1.事件命名
事件是表示发生在过去的事情,所以在命名上推荐使用Domain Name + 动词的过去式 + Event
,这样可以更准确地表达业务语义。
例如,在银行转账的例子中,对于转账成功和失败我们都需要发出事件通知,可以定义两个领域事件如下。
(1)MoneyTransferedEvent:表示转账成功发出的事件。
(2)MoneyTransferFailedEvent:表示转账失败发出的事件。
2.事件内容
事件内容在计算机术语中叫作payload,有以下两种形式。
(1)自恰(Enrichment):就是在事件的payload中尽量多放数据,这样consumer不需要回查就能处理消息,也就是自恰地处理消息。
(2)回查(Query-Back):这种方式是只在payload放置id属性,然后consumer通过回调的形式获取更多数据。这种形式会加重系统的负载,可能会引起性能问题。
领域实体的意义是有上下文的,比如同样是Apple,在水果店和苹果手机专卖店中表达出的含义就完全不一样。边界上下文(Bounded Context)的作用是限定模型的应用范围,在同一个上下文中,要保证模型在逻辑上统一,而不用考虑它是不是适用于边界之外的情况。
那么不同上下文之间的业务实体要如何实现交互呢?就像关系数据库和对象之间需要ORM一样,不同上下文之间的实体也需要映射。在DDD中,这种机制叫作上下文映射(Context Mapping),我们可以使用防腐层(Anti-Corruption)来完成映射的工作。
如图6所示,在我们开发的CRM系统中,商家的客户大部分是来自于ICBU网站的会员,虽然二者有很多属性都是一样的,但我们还是有必要引入防腐层来做上下文映射,主要有以下两个原因。
(1)虽然属性大部分一样,但二者的作用和行为在各自上下文中是不一样的。
(2)解耦影响,加入了防腐层之后,网站的会员变化就不会影响到CRM系统了。
#### 5. 领域模型设计:基于对象vs基于数据库
基于数据库,
在service层通过我们非常喜欢的manager去manage大部分的逻辑,POJO(稍后章节里的失血模型)作为数据在manager手(上帝之手)里不停地变换和组合
,service层在这里是一个巨大的加工工厂(很重的一层),围绕着数据库这份DNA,完成业务逻辑。
假设你的机器内存无限大,永远不宕机
,在这个前提假设下,我们是不需要持久化数据的,也就是我们可以不需要数据库,那么你将会怎么设计你的软件?这就是我们说的Persistence Ignorance:持久化无关设计
。
没了数据库,领域模型就要基于程序本身来设计了
,热爱设计模式的同学们可以在这里大显身手。在面向过程,面向函数,面向对象的编程语言中,面向对象无疑是领域建模最佳方式。
类与表有点像
领域模型并不完成业务,每个domain object都是完成属于自己应有的行为(single responsibility),就如同人跑这个动作,person.run是一个与业务无关的行为,但这个时候manger或者service在调用 some person.run的时候可能完成的100米比赛这个业务,也可能是完成跑去送外卖这个业务。这样的话形成了类似于如下的架构图:
我们回到假设,假设你的机器内存无限大,永远不宕机
,现在把假设去掉,没有谁的机器是内存无限大,永远不宕机的。去掉这个假设,我们需要数据库,但数据库的职责不再承载领域模型这个沉重的包袱了,数据库回归persistence的本质,完成以下两个事情:
领域模型是用于领域操作的,当然也可以用于查询(read),不过这个查询是代价的。在这个前提下,一个aggregate可能内含了若干数据,这些数据除了类似于getById这种方式,不适用多样化查询(query),领域驱动设计也不是为多样化查询设计的。
查询是基于数据库的,所有的复杂变态查询其实都应该绕过Domain层,直接与数据库打交道
。
再精简一下:````领域操作->objects, 数据查询->table rows。```
在开始进行抽象前一个必须的步骤就是“讲故事”!
讲什么故事呢?关于这个子域解决的业务问题或者提供的业务能力的故事。既然是故事,就必须有清晰的业务场景和业务对象之间的交互。这件事情看起来是如此自然和简单,然则一个团队里能够站起来有条不紊陈述清楚的却没有几人。读到这里的读者不妨停下来试试,你是否能够把现在你所做的业务在两三分钟内场景化地描述出来?
这么做显然目的是让我们能够比较完整地思考我们所要提炼和抽象的业务对象有哪些。只有当我们能够“讲”清楚业务场景的时候,才应该开始抽象的步骤。对于一个业务对象,我们常见的抽象可以是“实体”(Entity)
和“值对象”(Value Object)
。
用例分析法是进行领域建模最简单可行的方式。
Eric Evans的著作Domain-Driven Design领域驱动设计,简称DDD。DDD是一套软件开发方法论,用来解决复杂的现实问题。
四色建模法源于《Java Modeling In Color With UML》[10],它是一种模型的分析和设计方法,通过把所有模型分为四种类型,帮助模型做到清晰、可追溯。