DDD是什么
领域驱动设计(Domain Driven Design,DDD)是由 Eric Evans 最早提出的综合软件系统分析和设计的面向对象建模方法,如今已经发展成为了一种针对大型复杂系统的领域建模与分析方法。它完全改变了传统软件开发工程师针对数据库进行的建模方法,从而将要解决的业务概念和业务规则转换为软件系统中的类型以及类型的属性与行为,通过合理运用面向对象的封装、继承和多态等设计要素,降低或隐藏整个系统的业务复杂性,并使得系统具有更好的扩展性,应对纷繁多变的现实业务问题。
- 本质:一种系统分析和设计方法
- 区别:不同于针对数据库的建模方法,是面向领域实体
- 目标:用于降低系统复杂性
- 方法:将「业务概念」和「业务规则」转换为系统中的「类型」以及类型的「属性」与「行为」
- 核心:以领域为核心驱动力,领域驱动设计关注的焦点在于领域和领域逻辑,因为软件系统的本质其实是给客户(用户)提供具有业务价值的领域功能。
数据驱动设计VS服务驱动设计VS领域驱动设计
数据驱动设计关注的是数据表以及数据表之间关系的设计,是典型的面向技术实现的建模方法,面对日渐复杂的业务逻辑,这种设计方法欠缺灵活性与可扩展性,也无法更好地利用面向对象设计思想及设计模式,建立可重用的、可扩展的代码单元。
当我们从服务视角建立服务模型时,有两种不同的设计思想。一种思想是将服务视为一种资源,即 REST (REpresentational State Transfer,表述性状态迁移)架构风格的设计模式。采用这种设计思想建立的服务分析模型可以称之为“服务资源模型”。
领域模型驱动设计自然是以提炼和转换业务需求中的领域知识为设计的起点。在提炼领域知识时,没有数据库的概念,亦没有服务的概念,一切围绕着业务需求而来。尤其是领域建模的分析阶段,应该只关注问题域,模型表达的是业务领域的概念,而非实现的概念。
出发点 | 关注点 | 模型 | 所处层 | 技术方案 | 应用场景 | |
---|---|---|---|---|---|---|
数据驱动设计 | 从数据存储方案出发 | 怎么存储 | 实体关系建模,贫血模型,只有数据 | 基础设施层 | 根据存储方案密关联 | 简单业务场景,CRUD |
服务驱动设计 | 从服务提供的资源和行为能力出发 | 提供什么服务 | 接口对象建模,贫血模型,只有数据 | 应用层 | 不关注实现方案 | 简单业务场景,API服务 |
领域驱动设计 | 一切围绕着领域知识进行建模 | 领域划分和建模 | 领域对象建模,充血模型,包括数据和行为 | 领域层 | 不考虑存储方式(技术实现) | 复杂业务场景 |
领域驱动设计贯穿了整个软件开发的生命周期,包括对需求的分析、建模、架构、设计,甚至最终的编码实现,乃至对编码的测试与重构。分战略设计和战术落地两大部分:战略设计强调系统层面的架构模式,包括限界上下文、上下文映射、分层架构等,可以运用这些模式对整个系统的领域进行“分而治之”,从而降低业务复杂度,同时围绕“领域”为核心,建立业务复杂度与技术复杂度的边界;战术设计关注领域层面的设计模式,以“模型驱动设计”为主线,贯穿分析、设计与编码实现这三个不同的建模活动,并引入领域驱动设计的战术设计要素,如实体、值对象、领域服务、领域事件、聚合、资源库、工厂等。见下图:
DDD的思想总结
DDD是一种方法论,是“分治”思想的更详细描述,将复杂系统拆分成一个个尽可能垂直正交的小问题域,让每个问题域完整自己的职责,从而实现整体架构的整洁清晰。DDD本质还是遵循“高内聚低耦合”的原则,跟SOLID原则是遥相呼应的。DDD的战略设计站在一个更高更抽象的角度拆分问题域,战术设计原则则指导具体的落地实现细节。
战略设计
消化知识
做好一块业务的第一步是如何消化一个知识,消化一个知识其实就是建立有效模型,将知识透明化,变成可视化、可交流的文档或者图形。
在传统的瀑布方法中,业务专家与分析员进行讨论,分析员消化理解这些知识后,对其进行抽象并将结果结果传递给程序员,再由程序员编写软件代码。由于这种方法没有反馈,因此总是失败。分析员全权负责创建模型,但他们创建的模型只是基于业务专家的意见。他们既没有向程序员学习的机会,也得不到早期软件版本的经验。知识只是朝一个方向在流动,但是不会积累。
好的程序员会自然而然地抽象并开发出一个可以完成更多工作的模型。但是这个模型只是局部的,只能满足特定时刻特定部分的需求。只有团队所有成员一起消化理解模型,通过频繁的交互才能形成全局有效一致的认知。领域模型的不断精化迫使开发人员学习重要的业务原理,而不是机械地进行功能开发。
统一团队的领域交流语言
统一语言是提炼领域知识的产出物,获得统一语言就是需求分析的过程,也是团队中各个角色就系统目标、范围与具体功能达成一致的过程。
使用统一语言可以帮助我们将参与讨论的客户、领域专家与开发团队拉到同一个维度空间进行讨论,若没有达成这种一致性,那就是鸡同鸭讲,毫无沟通效率,相反还可能造成误解。因此,在沟通需求时,团队中的每个人都应使用统一语言进行交流。
一旦确定了统一语言,无论是与领域专家的讨论,还是最终的实现代码,都可以通过使用相同的术语,清晰准确地定义领域知识。重要的是,当我们建立了符合整个团队皆认同的一套统一语言后,就可以在此基础上寻找正确的领域概念,为建立领域模型提供重要参考。
统一语言体现在两个方面:
- 统一的领域术语
- 领域专家(业务),包括PM,技术团队,QA
- 名词,明确英文术语,为后续编码提供类,方法,属性等命名依据
- 避免统一领域概念的不同描述,建议的统一语言包括单不限于UML,数据表ER,接口定义。
- 领域行为描述
- 从领域的角度而非实现角度描述领域行为
- 若涉及到领域术语,必须遵循术语表的规范
- 动宾结构,符合业务动作在该领域的合理性
限界上下文
限界上下文的含义就是用一个清晰可见的边界(Bounded)将这个上下文勾勒出来,如此就能在自己的边界内维持领域模型的一致性与完整性。
我们需要根据业务相关性、耦合的强弱程度、分离的关注点对这些活动进行归类,找到不同类别之间存在的边界,这就是限界上下文的含义。上下文(Context)是业务目标,限界(Bounded)则是保护和隔离上下文的边界,避免业务目标的不单一而带来的混乱与概念的不一致。
限界上下文是一个“自治”的单元。所谓“自治”就是满足四个特征:最小完备、稳定空间、自我履行、独立进化。限界上下文,映射到编码实现,则可能是模块、组件或服务。
最小完备是实现“自治”的基本条件。所谓“完备”,是指自治单元履行的职责是完整的,无需针对自己的信息去求助别的自治单元,这就避免了不必要的依赖关系。
自我履行意味着由自治单元自身决定要做什么。从拟人的角度来思考,就是这些自治单元能够对外部请求做出符合自身利益的明智判断,是否应该履行该职责,由限界上下文拥有的信息来决定。
稳定空间指的是减少外界变化对限界上下文内部的影响。
独立进化与稳定空间刚好相反,指的是减少限界上下文的变化对外界的影响。
识别限界上下文
观察角度的不同,限界上下文划定的边界也有所不同。大体可以分为如下三个方面:
- 领域逻辑层面:限界上下文确定了领域模型的业务边界,维护了模型的完整性与一致性,从而降低系统的业务复杂度。
- 团队合作层面:限界上下文确定了开发团队的工作边界,建立了团队之间的合作模式,避免团队之间的沟通变得混乱,从而降低系统的管理复杂度。
- 技术实现层面:限界上下文确定了系统架构的应用边界,保证了系统层和上下文领域层各自的一致性,建立了上下文之间的集成方式,从而降低系统的技术复杂度。
这三种边界体现了限界上下文对不同边界的控制力。业务边界是对领域模型的控制,工作边界是对开发协作的控制,应用边界是对技术风险的控制。引入限界上下文的目的,其实不在于如何划分边界,而在于如何控制边界。
限界上下文之间的协作与通信
根据不同限界上下文的重要程度(相对业务目标而言)、限界上下文之间的关联强度(相对于用户感知而言)、限界上下文之间的顺序关系(相对于业务流程而言),采用不同的协作设计方式,尽可能减少不同限界上下文之间的同步循环依赖。
从设计的角度来讲,就是不遗余力地降低限界上下文之间的耦合关系。每个限界上下文都应该有防腐层(下游限界上下文,用以隔绝上游限界上下文可能发生的变化),针对关联强度不是很紧密的尽量采用发布/订阅事件的方式。
分离领域
分层的价值在于每一层都只代表程序中的某一特定方面。这种限制使每个方面的设计都更具内聚性,更容易解释。当然,要分离出内聚设计中最重要的方面,选择恰当的分层方式是至关重要的。尽管Layered Architecture的种类繁多,但是大多数成功的架构使用的都是下面4个概念层的某种变体。
层级 | 说明 |
---|---|
领域层(或模型层) | 负责表达业务概念,业务状态信息以及业务规则。尽管保存业务状态的技术细节是由基础设施层实现的,但是反映业务情况的状态是由领域层控制并且使用的领域层是业务软件的核心。 |
应用层 | 定义软件要完成的任务,并且指挥表达领域概念的对象来解决问题。这一层所负责的工作对业务来说意义重大,也是与其他系统的应用层进行交互的必要渠道 |
用户界面层(或表示层) | 负责向用户显示信息和解释用户指令。这里指的用户可以是另一个计算机系统,不一定是使用用户界面的人 |
基础设施层 | 为上面各层提供通用的技术能力;为应用层传递消息,为领域层提供持久化机制,为用户界面层绘制屏幕组件等等。基础设施层还能够通过架构框架来支持4个层次间的交互模式。 |
如果与领域有关的代码分散在大量的其他层(用户界面层、应用层、基础设施层等)之中,那么查看与分析领域代码就会变得异常困难。对用户界面的简单修改实际上很可能会改变业务逻辑。而要想调整业务规则也可能需要对用户界面代码、数据库操作代码或者其他的程序元素进行仔细的筛查。这样就不太可能实现一致的、模型驱动的对象了,同时也会给自动化测试带来困难。
给复杂的应用程序划分层次。在每一层内分别进行设计,使其具有内聚性并且只依赖于它的下层。采用标准的架构模式,只与上层进行松散的耦合。将所有与领域模型相关的代码放在一个层中,并把它与用户界面层、应用层以及基础设施层的代码分开。领域对象应该将重点放在如何表达领域模型上,而不需要考虑自己的显示和存储问题,也无需管理应用任务等内容。这使得模型的含义足够丰富,结构足够清晰,可以捕捉到基本的业务知识,并有效的使用这些知识。
各层之间是松散连接的,层与层的依赖关系只能是单向的。上层可以直接使用或操作下层元素,方法是通过调用下层元素的公共接口,保持对下层元素的引用,以及采用常规的交互手段。而如果下层元素需要与上层元素进行通信,则需要采用另一种通信机制,使用架构模式来连接上下层,如回调模式或观察者模式等。
战术设计
整个软件系统被分解为多个限界上下文(或领域)后,就可以分而治之,对每个限界上下文进行战术设计。领域驱动设计并不牵涉到技术层面的实现细节,在战术层面,它主要应对的是领域的复杂性。
领域驱动设计用以表示模型的主要要素包括:
- 实体(Entity)
- 值对象(Value Object)
- 聚合(Aggregate)
- 工厂(Factory)
- 资源库(Repository)
- 领域服务(Domain Service)
模型表示的元素
用于表示模型的3种模型元素模式:Entity、Value Object和Service。一个对象是用来表示某种具有连续性和标识事物,还是用于描述某种状态的属性,这是Entity和Value Objecct之间的根本区别。
领域中还有一些方面适合用动作或操作来表示,这比用对象表示更加清楚。这些方面最好用Service来表示,而不应把操作的责任强加到Entity或Value Object上,尽管这样做只是稍微违背了面向对象的建模传统。Service是接受客户端请求来完成某事。在软件技术层中有很多Service(这些不全是领域层所说的Service)。领域层的也可以使用Service,当对软件要做的某项无状态的活动进行建模时,就可以将该活动作为一项Service。
实体(Entity)
一些对象主要不是由它们的属性定义的,它们实际上表示了一条“标识线”,这些主要由标识定义的对象被称作Entity。Entity(实体)具有生命周期,这期间它们的形式和内容可能发生根本改变,但必须保持一种内在的连续性。
一个典型的实体应该具备三个要素:
- 身份标识
- 属性
- 领域行为
值对象(Value Object)
很多对象没有概念上的标识,但它们描述了一个事物的某种特征。用于描述领域的某个方面而本身没有概念标识的对象称为Value Object。
值对象通常作为实体的属性而存在,比如数量、性质、关系、地点、时间与形态等范畴。是否拥有唯一的身份标识才是实体与值对象的根本区别。
服务(Service)
有时候对象不是一个事物,在设计中会包含一些特殊的操作,这些操作从概念上讲不属于任何对象。与其把它们强制地归于哪一个类,不如顺其自然地在模型中引入一种新的元素,这就是Service(服务)。一些领域改变不适合被建模为对象。如果勉强把这些重要的领域功能归为Entity或Value Object的职责,那么不是歪曲基于建模的对象定义,就是人为增加一些无意义的对象。比如账户之间的转账,用转账服务来描述比将转账行为分解到账户中更加自然。
使用Service时应谨慎,它们不应该替代Entity和Value Object的所有行为。但是,一个操作实际上是一个重要的领域概念时,Service很自然就会称为Model-Driven Design中的一部分。
好的Service应该有以下3个特征:
- 与领域概念相关的不是Entity或Value Object 的一个自然组成部分。
- 接口是根据领域模型的其他元素定义的。
- 操作是无状态,这里说得无状态是指任何客户都可以使用某个Service的任何实例,而不必关心该实例的历史状态。
领域对象的生命周期
管理这些对象时面临诸多挑战,稍有不慎就会偏离Model-Driven Design的轨道。主要的挑战有以下两类:
- 在整个生命周期中维护完整性和事务。
- 防止模型陷入管理生命周期复杂性造成的困境当中。
通过3种模式解决这些问题,1)首先是Aggregate(聚合),它通过定义清晰的所属关系和边界,并避免错综复杂的对象关系网来实现模型的内聚。聚合模式对于维护生命周期各个阶段的完整性具有至关重要的作用。2)在生命周期的开始阶段,使用Factory(工厂)来创建和重建复杂对象,从而封装它们的内部结构。3)在生命周期的中间和末尾使用Repository(存储库)来提供查找和检索持久化对象并封装庞大基础设施的手段。尽管Repository和Factory本身并不是来源于领域,但它们在领域设计中扮演者重要的角色。
(聚合)Aggregate
Aggregate就是一组相关对象的集合,我们把它作为数据修改的单元。每个Aggregate都有一个根(root)和一个边界(boundary)。根是Aggregate所包含的一个特定Entity。对Aggregate而言,外部对象只可以引用根,而边界内部的对象之间则可以互相引用。
为了实现这个概念上的Aggregate,需要对所有事务应用一组规则。
- 根Entity具有全局标识,它最终负责检查固定规则。
- 根Entity具有全局标识。边界内的Entity具有本地标识,这些标识只在Aggregate内部才是唯一的。
- Aggregate外部的对象不能引用除Entity之外的任何内部对象。根Entity可以把内部Entity的引用传给它们,但这些对象只能临时使用这些引用,而不能保持引用。根可以把一个Value Object的副本传给另一个对象,而不必关系它发生什么变化,因为它只是一个Value,不再与Aggregate有任何关联。
- 作为上一条规则的推论,只有Aggregate的根才能直接通过数据库查询获取。所有其他对象必须通过关联来发现。
- Aggregate内部的对象可以保持对其他Aggregate根的引用。
- 删除操作必须一次删除Aggregate边界之内的所有对象。
- 当提交对Aggregate边界内部的任何对象的修改时,整个Aggregate的所有固定规则都必须被满足。
工厂(Factory)
当创建一个对象或创建整个Aggregate时,如果创建工作很复杂,或者暴露了过多的内部结构,则可以使用Factory进行封装。
复杂的对象创建是领域层的职责,然而这项任务并不属于那些用于表示模型的对象。当客户负责创建复杂对象时,它会牵涉不必要的复杂性,并将其职责搞的模糊不清。这违背了领域对象及所创建的Aggregate的封装要求。更严重的是,如果客户是应用的一部分,那么职责就会从领域层泄漏到应用层中。应用层与实现细节之间的这种耦合使得领域层抽象大部分优势荡然无存,而且导致后续更改的代价变得更加高昂。
Factory就是一种负责创建其他对象的构造机制,封装了创建复杂对象或Aggregate所需的知识。Factory提供了反映客户目标的接口,以及被创建对象的抽象视图,从而使客户无需知道对象的工作机理就可以使用对象的功能。
Factory有很多设计方式,包括但不限于工厂方法模式、抽象工厂模式和构造器模式。任何好的工厂都需要满足以下两个基本需求:
- 每个创建方法都是原子的,而且要保证被创建对象或Aggregate的所有固定规则。Factory生成的对象要处于一致的状态。在生成Entity时,意味着创建满足所有固定规则的整个Aggregate,但在创建完成后可以向Aggregate添加可选元素。
- Factory应该被抽象为所需的类型,而不是所要创建的具体类。
在向Aggregate添加元素时可以通过在Aggregate的根上创建一个Factory Method,从而把Aggregate的内部实现细节隐藏起来,使任何外部客户看不到这些细节,同时使根负责确保Aggregate在添加元素的完整性。
Factory与被构造对象之间是紧密耦合的,因此Factory应该只被关联到与被构造对象有着密切联系的对象上。当有些细节需要隐藏而又找不到合适的地方来隐藏它们时,必须创建一个专用的Factory对象或Service。整个Aggregate通常由一个独立的Factory来创建,Factory负责把对根的引用传递出去,并确保创建出的Aggregate满足固定规则。如果Aggregate内部的某个对象需要一个Factory,而这个Factory又不适合在Aggregate根上创建,那么应该构建一个独立的Factory。
资源库(Repository)
Factory封装了对象创建和重建时的生命周期转换。还有一种转换大大增加了领域设计的技术复杂性,就是对象与存储之间的互相转换。这种转换由另一种领域设计构造来处理,它就是Repository。
Repository将某种类型的所有对象表示为一个概念集合(通常是模拟的)。它的行为类似于集合,只是具有更复杂的查询功能。
Aggregate根提供Repository,让客户始终聚焦于模型,而将所有对象的存储和访问操作封装起来,在Repository里来完成。Respository有很多优点,包括:
- 它们为客户提供了一个简单的模型,可用来获取持久化对象并管理它们的生命周期。
- 它们使应用程序和领域设计与持久化技术(多种数据库策略甚至是多个数据源)解构。
- 它们体现了有关对象访问的设计决策
- 可以很容易将它们替换为“哑实现”,以便在测试中使用(通常使用内存中的集合)
参考
- 《实现领域驱动设计》Vaughn Vernon
gitchat:领域驱动设计实践(战略篇),需要付费购买, 张逸
gitchat:领域驱动设计实践(战术篇),需要付费购买, 张逸