DDD (Domain-Driven Design),即领域驱动设计是思考问题的方法论,用于对实际问题建模,它以一种领域专家、设计人员、开发人员都能理解的通用语言作为相互交流的工具,然后将这些概念设计成一个领域模型。由领域模型驱动软件设计,用代码来实现该领域模型。所以,DDD 的核心是建立正确的领域模型。
定义上说,领域模型是对具有某个边界的领域的一个抽象,反映了领域内用户业务需求的本质;领域模型是有边界的,只反映了我们在领域内所关注的部分,确保了我们的软件的业务逻辑都在一个模型中,这样对提高软件的可维护性、业务s可理解性以及可重用性方面都有很好的帮助。领域模型只反映业务,和任何技术实现无关,它不仅能反映领域中的一些实体概念(如货物,书本,应聘记录,地址等),还能反映领域中的一些过程概念(如资金转账等)。 领域模型贯穿软件分析、设计,以及开发的整个过程,领域专家、设计人员、开发人员通过领域模型进行交流,彼此共享知识与信息。了让领域模型看的见,我们需要用一些方法来表示它。图是表达领域模型最常用的方式,但并不是唯一的表达方式,代码、文字描述也能表达领域模型。
领域驱动设计的一个核心的原则是使用一种基于模型的语言。因为模型是软件满足领域的共同点,它很适合作为这种通用语言的构造基础。使用模型作为语言的核心骨架,要求团队在进行所有的交流是都使用一致的语言(包括演讲、文字和图形),需要确保团队使用的语言在所有的交流形式中看上去都是一致的,这种语言被称为**通用语言 (Ubiquitous Language)**。在建模过程中,通用语言广泛尝试于推动软件专家和领域专家之间的沟通,从而发现要在模型中使用的主要的领域概念。
例:以 DDD 之父的货物运输系统为例,他描述领域模型如下:
由上面可以看到,上述这段描述完全以货物为中心,把客户看成是货物在某个场景中可能会涉及到的关联角色。他完全没有从用户的角度去描述领域模型,而是以领域内的相关事物为出发点,考虑这些事物的本质关联及其变化规律的。 其实如果以用户为中心来思考领域模型的思维,这只是停留在需求的表面,而没有挖掘出真正的需求的本质。我们在做领域建模时需要努力挖掘用户需求的本质,这样才能真正实现用户需求。
关联本身不是一个模式,但它在领域建模的过程中非常重要,所以需要在探讨各种模式之前,先讨论一下对象之间的关联该如何设计。我觉得对象的关联的设计可以遵循如下的一些原则:
实体就是领域中需要唯一标识的领域概念。假设有两个实体,如果唯一标识不一样,那么即便实体的其他所有属性都一样,我们也认为他们两个不同的实体。因为实体有生命周期,实体从被创建后可能会被持久化到数据库,然后某个时候又会被取出来。所以,如果我们不为实体定义一种可以唯一区分的标识,那我们就无法区分到底是这个实体还是哪个实体。 我们不应该给实体定义太多的属性或行为,而应该寻找关联,发现其他一些实体或值对象,将属性或行为转移到其他关联的实体或值对象上。
例:
在领域中,并不是每一个事物都必须有一个唯一标识,也就是说我们不关心对象是哪个,而只关心对象是什么,这种事物被称为值对象 (Value Object)。值对象与实体的区别在于:
例: 就以上面的地址对象为例,如果有两个客户的地址信息是一样的,我们就会认为这两个客户的地址是同一个。也就是说只要地址信息一样,我们就认为是同一个地址。用程序的方式来表达就是,如果两个对象的所有的属性的值都相同我们会认为它们是同一个对象的话,那么我们就可以把这种对象设计为 Value Object。 我们应该给值对象设计的尽量简单,不要让它引用很多其他的对象,因为他只是一个值,就像 int a = 3 一样,”3” 就是一个我们传统意义上所说的值,而值对象其实也可以和这里的 ”3” 一样,理解为一个对象形式的值。所以,当我们在 Java 语言中比较两个值对象是否相等时,会重写 hashCode 和 equals 这两个方法,目的就是为了比较对象的值; 虽然 Value Object 是只读的,但是可以被整个替换掉。
领域中的一些概念不太适合建模为对象,因为它们本质上就是一些操作或者动作,而不是事物。这些操作或动作往往会涉及到多个领域对象,并且需要协调这些领域对象共同完成这个操作或动作,所以我们需要寻找一种新的模式来表示这种跨多个对象的操作。DDD 认为服务是一个用来对应这种跨多个对象的操作,同时又是很自然的范式,所以就有了领域服务这个模式。 一般的领域对象都是有状态和行为的,而领域服务没有状态只有行为。它存在的意义就是协调领域对象共完成某个操作,所有的状态还是都保存在相应的领域对象中。模型(实体)与服务(场景)是对领域的一种划分,模型关注领域的个体行为,场景关注领域的群体行为;模型关注领域的静态结构,场景关注领域的动态功能。这也符合了现实中出现的各种现象,有动有静,有独立有协作。 领域服务还有一个很重要的功能,就是可以避免领域逻辑泄露到应用层。因为如果没有领域服务,那么应用层会直接调用领域对象,完成本该是属于领域服务该做的操作,这样一来,领域层可能会把一部分领域知识泄露到应用层。对于应用层来说,通过调用领域服务,提供的简单易懂但意义明确的接口肯定也要比直接操纵领域对象容易的多。
注:软件中一般有三种服务,以转账业务为例:
聚合 (Aggregate) 通过定义对象之间清晰的所属关系和边界,实现领域模型的内聚,并避免了错综复杂的、难以维护的对象关系网的形成。通过聚合,可以定义一组具有内聚关系的相关对象集合,我们把聚合看作是一个修改数据的单元。 对于一个聚合,用一个实体作为唯一表示,那么这个实体就是聚合根 (Aggregate Root)。聚合根负责与外部其他对象打交道,并维护自己内部的业务规则。换句话说,聚合根对于聚合而言,相当于数据库表的主键字段。
聚合与聚合根的特点如下:
在辨别聚合与聚合根时,可以通过如下角度思考问题:
注:
DDD 中引入工厂模式的原因是,有时创建一个领域对象是一件比较复杂的事情,不仅仅是简单的 new 操作。正如对象封装了内部实现一样(我们无需知道对象的内部实现就可以使用对象的行为),工厂则是用来封装创建一个复杂对象尤其是聚合时所需的知识,它将创建对象的细节隐藏起来。客户传递给工厂一些简单的参数,然后工厂可以在内部创建出一个复杂的领域对象,然后返回给客户。 工厂在创建一个复杂的领域对象时,通常会知道该满足什么业务规则(它知道先怎样实例化一个对象,然后在对这个对象做哪些初始化操作,这些知识就是创建对象的细节),如果传递进来的参数符合创建对象的业务规则,则可以顺利创建相应的对象;但是如果由于参数无效等原因不能创建出期望的对象时,应该抛出一个异常,以确保不会创建出一个错误的对象。 我们也并不总是需要通过工厂来创建对象,事实上大部分情况下领域对象的创建都不会太复杂,所以我们只需要简单的使用构造函数创建对象就可以了。隐藏创建对象的好处是显而易见的,这样可以不会让领域层的业务逻辑泄露到应用层,同时也减轻了应用层的负担,它只需要简单的调用领域工厂创建出期望的对象即可。
领域模型中的对象自从被创建出来后不会一直留在内存中活动的,当它不活动时会被持久化到数据库中,然后当需要的时候我们会重建该对象;重建对象就是根据数据库中已存储的对象的状态重新创建对象的过程;所以,可见重建对象是一个和数据库打交道的过程。从更广义的角度来理解,我们经常会像集合一样,从某个类似集合的地方,根据某个条件获取一个或一些对象,往集合中添加对象或移除对象。也就是说,我们需要提供一种机制,可以提供类似集合的接口来帮助我们管理对象。仓库 (Repository) 就是基于这样的思想被设计出来的。 仓库里面存放的对象一定是聚合。原因是之前提到的领域模型中是以聚合的概念去划分边界的;聚合是我们更新对象的一个边界,事实上我们把整个聚合看成是一个整体概念,要么一起被取出来,要么一起被删除。我们永远不会单独对某个聚合内的子对象进行单独查询或做更新操作。因此,我们只对聚合设计仓库。此外,尽管仓库可以像集合一样在内存中管理对象,但是仓库一般不负责事务处理。一般事务处理会交给工作单元 (Unit Of Work)。关于工作单元的详细信息我在下面的讨论中会讲到。
领域建模是一个不断重构,持续完善模型的过程,是领域专家、设计人员、开发人员之间沟通交流的过程,是工作和思考问题的基础。大家会在讨论中将变化的部分反映到模型中,从而使模型不断细化,并朝正确的方向走。设计领域模型的一般步骤如下:
1:1, 1:N, M:N
这些关系。这个原则强调了聚合的真正用途:除了封装我们本身所关心的信息外,聚合最主要的目的是为了封装业务规则,保证数据的一致性。当我们在设计聚合时,要多想想当前聚合封装了哪些业务规则,实现了哪些数据一致性。
注:业务规则比如:一个银行账号的余额不能小于 0,订单中的订单明细的个数不能为 0,订单中不能出现两个明细对应的商品 ID 相同,订单明细中的商品信息必须合法,商品的名称不能为空,回复被创建时必须要传入被回复的帖子等;
这个原则更多的是从技术的角度去考虑的。举个例子,某个聚合在一开始时设计的很大,包含了很多实体,但是后来发现因为该聚合包含的东西过多,导致多人操作时并发冲突严重,导致系统可用性变差。后来开发团队将原来的大聚合拆分为多个小聚合,当然拆分为小聚合后,原来在大聚合中维护的业务规则同样在多个小聚合上有所体现,所以既能解决并发冲突的问题,也能保证让聚合来封装业务规则,实现模型级别的数据一致性。 小聚合的设计还有一个好处,就是业务决定聚合,业务改变聚合。小聚合的设计除了可以降低并发冲突的可能性之外,同样减少了业务改变时聚合的拆分个数,降低了聚合大幅重构(拆分)的可能性,从而能让我们的领域模型更能适应业务的变化。
这个原则是考虑到,聚合之间无需通过对象引用的方式来关联。
聚合内强一致性,聚合之间最终一致性。聚合如果只需要关注如何实现业务规则,不需要考虑实现查询需求所带来的好处,也就是说,我们不需要在 domain 里维护各种统计信息,而只要维护各种业务规则所潜在的必须依赖的状态信息即可。 举个例子,假如一个论坛,有版块和帖子,以前,我们可能会在版块对象上有一个帖子总数的属性,当新增一个帖子时,会对这个属性加 1;而在 CQRS 架构下,领域层聚合根无需维护总帖子数的统计信息了,总帖子数会通过某些查询条件,通过数据库的查询语句获得,领域层只需要提供查询条件即可。
注:该部分指的是,数据录入可以看作是一个聚合,汇总统计可以看作另外一个聚合。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wfW1kIbH-1594516174925)(./pic/DDD经典分层结构.png)]
上图为经典的领域驱动设计分层架构,图中领域层 (Domain) 的上层是应用层 (Application),下层是基础设施层 (Infrastructure),那么领域层与其他层如何交互?
对于应用层,首先会启动一个工作单元,然后分情况进行操作:
注:
前面说到工作单元 (Unit of Work),实现方法如下:
限界上下文 (Bounded Context) 是一个显式的边界,领域模型便存在于这个边界之内。在边界内通用语言中,所有术语和词组都有特定的含义,而模型需要准确地反映通用语言。 不要试图去创建一个“大而全”的软件模型,导致每个概念在全局范围内只有一种定义。最好的方法是正视这种不同,然后用限界上下文对领域模型进行分离。
注:简单点考虑,可以将限界上下文想象成技术组件,但技术组件并不能定义限界上下文。比如在 IDEA 中,一个限界上下文通常就是一个工程项目。
多个系统之间会发生关系,存在交互,这也必然会在各自的限界上下文有所表现。上下文图 (Context Map) 便是表示各个系统之间关系的总体视图。上下文图有如下几种形式来表示限界上下文之间的关系。
当不同团队开发一些紧密相关的应用程序时,团队之间需要进行协调,通常可以将两个团队共享的子集剥离出来形成共享内核 (Shared Kernel),双方进行持续集成。由此可见共享内核是业务领域中公共的部分,同时也是团队间容易达成且必须达成共识的领域部分。
不同系统之间存在依赖关系时,下游系统依赖上游系统,下游系统是客户,上游系统是供应商,双方协定好需求,由上游系统完成模型的构建和开发,并交付给下游系统使用,之后进行联调、测试。这种模式建立在团队之间友好合作和支持的情况下。
如果上游系统不合作,这时候“客户/供应商”模式就不凑效了,那么下游系统只能去追随上游系统,下游系统严格遵从上游系统的模型,简化集成。
例如在社区系统中,社区服务的打赏记录来源于支付系统,支付系统的打赏记录这一模型设计较为友好,且社区服务只是一个简单的查询模型,可以直接追随支付系统的打赏记录模型,集成简单快速。
如果上游系统的模型不友好,不适合下游系统的场景,但是下游系统又必须依赖于这些模型,这时候我们需要使用防腐层 (Anticorruption Layer) 模式将上游系统的影响降低。这种模式也是非常常见的,通常出现在系统间对接时,使用 transport + resolver 的方式完成服务调用和协议转换。其中的 resovler 便承担了防腐的作用。
公开主机服务 (Open Host Service) 能够允许系统将一组 service 公开出去,供其他系统访问,在互通模型的同时,减少了系统间的耦合。此类模式是使用最多的。系统之间的交互通常是使用该模式来完成的,微服务架构、注册中心就是此类模式的实现形式。
当两个系统之间的关系并非必不可少时,两者完全可以彼此独立,各自独立建模,独立发展,互不影响。
综合上述模式,由上而下耦合越来越低,模型的共享程度也越来越低,在实践中,使用得最多的应当是:防腐层 + 公开主机服务的搭配使用。 实际开发中,一个上游系统会面对多个下游系统做过多定制化的建模,且由于团队组织和管理上的天然隔离,团队合作的紧密度通常并不会那么高,因此,要做到“共享内核”和“客户/供应商”模式是比较困难的。如今发展的如火如荼的微服务架构便是**“防腐层 + 公开主机服务”**的实现形式,使用 RESTful 契约公开主机服务,在业务模型上使用防腐层完成隔离和适配,达到共享模型和降低耦合的平衡。
“共享内核”模式在通用业务领域较为常见,在细分业务领域中,通常由几家厂商抽象出通用的业务模型,随后进行产品化,售卖给各个甲方企业。在实施集成过程中,根据甲方的个性化诉求,在“共享内核”上做二次开发。 “客户/供应商”模式个人觉得实施起来会比较困难,毕竟是跨团队协作,即使领头上司是一个,如果这个关键人物不去把控系统设计,那么业务模型上的一致性是很难保证的,最后估计会演变为“防腐层”模式;如果这个关键人物会实际参与到系统建模和设计中,那实际上编程了一个大的团队了,也就无所谓“客户/供应商”模式了,都是自己了。