背景
DDD到底解决了什么问题?它是怎么演化出来的?我们先来看看传统的三层架构
image.png
说明
| 层级 | 功能 | 职责 |
|---|---|---|
| 表示层(Presentation Layer) | 处理用户界面和用户输入。 | 展示数据,处理用户交互,将用户的请求传递给业务逻辑层,并显示结果。 |
| 业务逻辑层(Business Logic Layer) | 包含应用程序的核心业务逻辑和规则。 | 处理业务逻辑,实现应用程序的核心功能,调用数据访问层进行数据操作。 |
| 数据访问层(Data Access Layer) | 处理与数据库或其他存储系统的交互。 | 执行 CRUD 操作,管理数据持久化,封装对存储的访问逻辑。 |
存在的问题
熵增定律:熵的概念最早起源于物理学,热力学第二定律(又称“熵增定律”),表明了在自然过程中,一个孤立的系统总是从最初的集中、有序的排列状态,趋向于分散、混乱和无序;当熵达到最大时,系统就会处于一种静寂状态。软件系统亦是如此, 在软件系统的维护过程中。软件的生命力会从最初的集中、有序的排列状态,逐步趋向复杂、无序状态,直到软件不可维护而被迫下线或重构。
三层架构重Service,所有业务逻辑写到Service里面,还包含了调用各种中间件(如缓存,MQ,数据库等),随着业务不断迭代.需求不断变更.比如以订单退款逻辑为例,包括订单确认前/后退款、履约前/后退款等多种场景,以及需要考虑由用户、商家、客服和系统等不同角色发起的退款。虽然这些不同场景和角色发起的退款业务逻辑在很大程度上是相似的,但它们之间也存在一些差异。
在传统的MVC架构模式下,由于缺乏对业务领域的深入理解和沉淀,服务间的调用往往缺乏清晰的结构,导致逻辑交织在一起。此外,研发团队在系统迭代过程中可能没有足够重视高内聚和低耦合的设计原则。因此,系统内部往往会出现多处重复且相似的订单退款代码逻辑,这不仅降低了系统的可读性,也给系统的可维护性带来了挑战。结局往往就是代码慢慢变成了屎山,要么出现大泥球(很长的业务逻辑,各种if else),要么就是逻辑散落在各个服务(各种复制粘贴,即使只有微小的差异),牵一发而动全身。
问题分析
我们可以先从软件的复杂性进行分析
image.png
● 不确定性
○ 不确定性的来源包括业务的不确定(需求不断迭代演化),技术的不确定性(技术的不断发展进步),组织的不确定性(人员的流动)
● 无序性
○ 系统和代码像多个线团一样散落一地一样,混乱不堪,毫无头绪
● 规模
○ 业务规模的膨胀以及开发团队规模的膨胀,都会带来系统的复杂性提升
● 认知成本
○ 是指开发人员需要多少知识才能完成一项任务。在引入新的变化时,要考虑到带来的好处是否大于系统认知成本的提升
解决思路
面对不确定性
专注问题内核(领域抽象),以业务为核心,分离业务复杂度和技术复杂度。
● 业务与技术的隔离
● 内部系统与外部依赖的隔离
● 系统中常变部分与不常变部分的隔离
● 隔离复杂性(把复杂性的部分隔离在一个模块,尽量不与其他模块互动)
面对无序性
● 统一认知(秩序化)
● 系统清晰明了的结构(结构化)
● 业务开发流程化(标准化)
面对规模
● 分层分治分离
面对认知成本
● 系统的有序性, 架构清晰
● 代码的含义清晰,不模糊,整洁
● 避免过度设计,不合适的设计模式也是增加认知成本的一种,减少复杂、重复概念, 降低学习成本
● 谨慎引入会带来系统复杂性的变化
DDD(领域驱动设计)
DDD与MVC架构的映射关系
我们先来看下三层架构和DDD架构的映射关系,如图:
image.png
说明
| 层级 | 功能 | 职责 |
|---|---|---|
| 接口层(适配层) | 处理用户界面和用户输入 | 展示数据,处理用户交互,将指令(可以是http指令、rpc指令、事件指令、自动化测试指令、批处理脚本指令等)传递给应用层,并显示结果。 |
| 应用层 | 实现应用程序的用例和业务流程,确保应用程序的工作流正确无误 | 很薄的一层,理论不应该存在业务规则或逻辑,主要面向用例和流程相关操作,编排领域层中聚合、领域服务以及外部服务,协同完成业务操作。 |
| 领域层 | 包含领域模型和业务逻辑,是DDD的核心层 | 内敛核心业务逻辑,体现领域模型的业务能力,它用来表达业务概念、业务状态和业务规则。 |
| 基础设施层 | 处理资源操作 | 贯穿所有层,为其他各层提供技术和基础服务支持,包括第三方工具、驱动、消息中间件、网关、文件、缓存以及数据库等。比较常见的功能还是提供数据库持久化。 |
领域服务(Domain Service)
领域服务,通常用于处理跨越多个聚合的业务逻辑,或者,处理那些没有明显的归属到某个特定对象的逻辑。领域服务通常包含:复杂的业务逻辑,它们负责协调多个领域对象之间的交互。
比如:在电商系统中,OrderService 可以作为领域服务来处理跨多个订单的复杂业务逻辑。
聚合(Aggregate)
聚合是一个领域模型中的重要概念,它是一个实体的根,负责管理:聚合内所有的实体、和值对象。
比如:订单聚合,就可以包含:订单、物流、发货地址...等。
聚合中所包含的对象之间具有密不可分的联系,一个聚合中可以包含多个实体和值对象,因此聚合也被称为根实体。聚合是持久化的基本单位,它和资源库具有一一对应的关系。在聚合中,根是唯一允许外部对象保持对它的引用的元素,而边界内部的对象之间则可以互相引用。除根以外的其他 Entity 都有本地表示,但这些标识只有在聚合内部才需要加以区别,因为外部对象除了根 Entity 之外看不到其他对象。在一个聚合中直接引用另外一个聚合并不是 DDD 所鼓励的,但是我们可以通过 ID 的方式引用另外的聚合,聚合是一个事务的边界。如果一次业务操作涉及到了对多个聚合状态的更改,那么应该采用发布领域事件的方式通知相应的聚合。此时的数据一致性便从事务一致性变成了最终一致性(Eventual Consistency)
实体(Entity)
实体是领域模型中具有唯一标识符的对象。并且通过唯一标识来区分其他实体(即使它们的其他属性相同)
实体在系统中有自己的生命周期,可以被创建、修改和删除。
比如我们常见的订单就是一个实体。
public class Order {
public Guid Id { get; private set; }
public DateTime OrderDate { get; private set; }
public string CustomerId { get; private set; }
public List<OrderItem> Items { get; private set; }
// 其他属性和方法
public void AddItem(OrderItem item) {
// 添加商品到订单
}
}
值对象(ValueObject)
值对象,代表某种特定的概念、或属性,并且通常是不可变的。
值对象一旦创建,其属性值不会改变,如果需要更改,通常会创建一个新的值对象。
当我们只关心一个模型元素的属性时,应把它归类为值对象。应该使这个模型元素能够表示出其属性的意义,并为它提供相关功能。建议将值对象设计成一个不变(Immutable)对象,这样就不需要担心并发带来的诸如同步、冲突等问题了,这既降低了编程的难度,又可以无需引入额外的同步锁影响程序的性能。也不要为它分配任何标识,这样应用也无需去管理值对象的生命周期。值对象通过比较其属性(equals)区分是否是相同值对象。应该尽量使用值对象来建模而不是实体对象。在领域驱动设计中,提倡尽量定义值对象来替代基本类型,因为基本类型无法体现统一语言中的领域概念。假设一个实体定义了许多属性,这些属性都是基本类型,就会导致与这些属性相关的领域行为都要放到实体中,导致实体的职责变得不够单一。引入值对象后情况就不同了,我们可以利用合理的职责分配,将这些职责(领域行为)按照内聚性分配到各个值对象中,这个领域模型就能变得协作良好。值对象可以与其所在的实体对象保存在同一张表中,值对象的每一个属性保存为一列;值对象也可以独立于其所在的实体对象保存在另一张表中,值对象获得委派主键,该主键对客户端是不可见的。
仓储(Repository)
mysql、redis、mq,他们的共性是数据存储与传递,那为什么dao操作mysql,而redis和mq的操作确要放在service?是否可以抽离出一个基础设施层来完成这个事情?这就是仓库的作用,它主要封装了对资源的操作.同时我们也可以看到领域服务依赖了Repository(抽象),而基础设施层提供了具体的Repository实现.这样做的好处就是service层与技术解耦,未来组件升级不用动service层,替换组件更加容易,如mysql替换为MongoDB,mq从卡夫卡替换rocketMQ
Repository用于保存和获取聚合对象,将实际的存储和查询技术封装起来,对外隐藏封装了数据访问机制。只为那些确实需要直接访问的聚合提供 Repository。让客户始终聚焦于模型,而将所有对象的存储和访问操作交给 Repository 来完成。资源库与 DAO 有些相似,但也存在显著区别,DAO 是比 Repository 更低的一层,同时 DAO 只是对数据库的一层很薄的封装,而资源库则更加具有领域特征,以“领域”为中心,所描述的是“领域语言”。另外,所有的实体都可以有相应的 DAO,但并不是所有的实体都有资源库,只有聚合才有相应的资源库。
领域事件(Domain Event)
用于表示领域模型中发生的业务事件。
例如,当订单被创建时,可以触发:库存管理系统,更新库存...等事件。
以及,当用户信息被更新时,可以发布领域事件,以同步到其他相关系统。
在传统的软件系统中,对数据一致性的处理都是通过事务完成的,其中包括本地事务和全局事务。DDD 的一个重要原则便是一次事务只能更新一个聚合实例,但存在一个业务流程涉及修改多个聚合的事务,怎么实现整个业务流程的数据一致性呢?在 DDD 中,领域事件便可以用于处理上述问题,此时最终一致性取代了事务一致性,通过领域事件的方式达到各个组件之间的数据一致性。既然是领域事件,他们便应该从领域模型中发布,一个领域事件是指一个在领域中“有意义”的事件。领域事件的最终接收者可以是本限界上下文中的组件,也可以是另一个限界上下文。再进一步发展,事件驱动架构可以演变成事件源(Event Sourcing),即对聚合的获取并不是通过加载数据库中的瞬时状态,而是通过重放发生在聚合生命周期中的所有领域事件完成。
对象概念
image.png
VO(View Object):视图对象,用于展示层,它的作用是把某个指定页面(或组件)的所有数据封装起来。
DTO(Data Transfer Object):数据传输对象,分布式应用提供粗粒度的数据实体,也是一种数据传输协议,以减少分布式调用的次数,从而提高分布式调用的性能和降低网络负载,这里泛指用于展示层与服务层之间的数据传输对象。RPC(如Dubbo)对外暴露的服务涉及对象 API 就是 DTO,对比 VO:绝大多数应用场景下,VO 与 DTO 的属性值基本一致,但对于设计层面来说,概念上还是存在区别,DTO 代表服务层需要接收的数据和返回的数据,而 VO 代表展示层需要显示的数据。
DO(Domain Object):领域对象,就是从现实世界中抽象出来的有形或无形的业务实体。DO 不是简单的 POJO,它具有领域业务逻辑。
PO(Persistent Object):持久化对象。对比 DO:DO 和 PO 在绝大部分情况下是一一对应的,但也存在区别,例如 DO 在某些场景下不需要进行显式的持久化,只驻留在静态内存。同样 PO 也可以没有对应的 DO,比如一对多表关系在领域模型层面不需要单独的领域对象。
DDD最终架构
image.png
DDD核心思想解读
分层分治分离
比如划分不同的子域,同一个子域内划分不同的限定上下文,针对同一个上下文内设计领域服务,最领域对象设计
image.png
DDD实施过程
image.png
需求分析
业务价值分析有助于评估系统的复杂性,并且可以指导我们识别最为关键的业务领域。业务需求分析是一个关键的知识提炼过程,其中涉及多种方法和工具,例如事件风暴、四色建模以及用例分析等,我们常采用的是相对轻量的用例分析法。
用例分析
对角色,场景,业务流程进行细致的分析。通过拆解业务流程和环节,帮助我们发现和识别具体的业务用例。比如我们常见的交易业务流程的分析
image.png
关键概念抽象
通过用例分析,我们对这些关键的概念进行归类,识别出了商家、商品、订单、优惠和结算等几个子域。
image.png
子域划分
在问题域分析阶段主要的输出包括两大部分:一是统一语言,二是子域划分。
- 在统一语言上,通过用例分析我们提炼了商家、买家、商品等统一语言,通过用例规约的整理对统一语言进行了丰富,包括售卖规则、售卖单元、订单项等等,我们可以使用这些统一语言进行交流并且用于后面的模型设计和代码实现。
-
在子域划分上,我们最终识别出了如图所示的这样几个子域,结合我们在价值分析阶段得到的为用户提供一站式服务体验,以及为商家提供一体化售卖平台的这样的核心价值,我们将商品域和订单域作为核心域进行重点建设。
image.png
限界上下文
限定上下文识别
我们在限界上下文识别的时候,也主要是从业务边界和应用边界两方面来进行。首先我们基于语义相关性和功能相关性对我们在问题域分析阶段所罗列的业务活动进行归类,优先考虑功能相关性,得到初步的限界上下文划分。
业务边界
以商品为例,其涉及到商品的创建、审核发布以及用户端的展示销售等环节。在商品创建阶段,商家关注录单效率和商品制作过程的管理;在审核阶段,运营关注审核需求和审核效率;而在展示销售阶段,用户关注商品信息、价格库存以及如何做出购买决策。订单的情形也类似,在购买、履约和售后各个阶段,关注点也有所不同。因此,我们需要对商品和订单的限界上下文进行细分,以确保系统设计能够更精准地满足各阶段的业务需求。
image.png
应用边界
从质量属性、服务集成和功能复用三个方面对限界上下文做进一步的划分,以商品计算为例,商品计算量大、任务多、规则复杂,为了避免影响正常的商品展示和售卖,所以从展销上下文进行了拆解。此外,我们的商品和订单都涉及到要与很多第三方的系统进行对接,这里面将第三方服务的集成划分为单独的直连上下文,从而隔离三方系统差异对内部商品和订单相关系统带来的变化。在功能复用上,我们考虑对多个限界上下文都涉及的功能进行提炼,作为单独的一个上下文,比如商家权限上下文。
image.png
限定上下文特征
image.png
● 最小完备 是实现自治的基本条件,指的是自治单元履行的职责是根据业务价值的完整性和最小功能集进行设计的,这让自治单元无需求助其他自治单元获得信息,避免了不必要的依赖关系,同时也避免了不必要和不合适的职责添加到该自治单元上。
● 自我履行意味着由自治单元自身决定要做什么。是否应该履行某职责,由限界上下文拥有的信息来决定。站在自治单元的角度去思考:“如果我拥有了这些信息,我究竟应该履行哪些职责?”这些职责属于当前上下文的活动范围,一旦超出,就该毫不犹豫地将不属于该范围的请求转交给别的上下文。自我履行其实意味着对知识的掌握,为避免风险,你要履行的职责一定是你掌握的知识范畴之内。
● 稳定空间 指的是减少外界变化对限界上下文内部的影响。稳定空间符合开放封闭原则(OCP),即对修改是封闭的,对扩展是开放的,该原则其实体现了一个单元的封闭空间与开放空间。封闭空间体现为对细节的封装与隐藏,开放空间体现为对共性特征的抽象与统一,二者共同确保了整个空间的稳定。
● 独立进化 指的是减少限界上下文的变化对外界的影响。用限界上下文的上下游关系来阐释,则稳定空间寓意下游限界上下文,无论上游怎么变,我自岿然不动。要做到独立进化,就必须保证对外公开接口的稳定性,因为这些接口被众多消费者依赖和调用,一旦发生变更,就会牵一发而动全身。一个独立进化的限界上下文,需要一个稳定、设计良好的接口设计,并在版本上考虑了兼容与演化。
限定上线文验证
● 正交原则
● 奥卡姆剃刀原则
限定上线文映射
限界上下文封装了按照纵向切分的业务能力,那多个限界上下文如何协作来完成一个完整的业务场景呢,这就涉及到限界上下文的映射,按照通信集成模式和团队协作模式来划分,有多种映射关系,这里面我们用到最多的是通过防腐层、开放主机服务和发布语言三者联动来隔离上下游的变化、维护整个领域模型的稳定性。
image.png
● 防腐层设计:
防腐层(Anti-Corruption)是一种高度防御性的策略,结合门面(Facade)模式和适配器(Adapter)设计模式,将模型与其需要集成的其他模型隔离开来,以防止被频繁变更或不稳定的依赖模型污染和腐败。
image.png
● 开放主机服务(Open Host Service)
开放主机服务定义公开服务的协议,包括通信方式、传递消息的格式(协议),让限界上下文可以被当做一组服务访问。它也可以视为一种承诺,保证开放的服务不会轻易做出变化。开放主机服务是一种上下文关系的映射,也称为上下游关系映射或API调用,一个上下文通过RPC等同步方式调用另外一个上下文的API,调用者是被调用者的客户端。这种方式在微服务架构中比较普及,两个上下文通过服务接口耦合在一起。
● 发布语言(Published Language)
发布语言是用于两个限界上下文之间的模型转换的公共语言。它是一种大家都能够理解、解释的语言,很多行业基于XML建立各自行业的标准语言。发布语言确保不同上下文之间的通信是基于一种共同理解的语言进行的,从而避免直接依赖于对方的领域模型。防腐层和开放主机服务操作的对象都不应该是各自的领域模型,这正是引入发布语言的原因。
领域建模
在领域建模阶段,整体上分为领域分析建模和领域设计建模。
● 领域分析建模
对用例以及用例规约和用户故事进行详细的分析,从中通过名词法和动词法寻找概念对象.同时识别语义关系,添加关联关系.
● 领域设计建模
基于概念模型,识别出这些概念中的实体和值对象,丰富领域行为,并且根据业务规则的不变性设计聚合,同时添加领域服务和领域事件。
以订单为例,这里是我们简化之后的模型,包括订单、支付单、履约单、凭证以及退款单这样几个聚合,在存在状态变化时,聚合之间通过领域事件进行协作。
image.png
架构映射
image.png
DDD的几种架构实践
image.png
六边形架构
又称为端口-适配器,六边形架构也是一种分层架构,不是从上下或左右分,而是从内部和外部来分。六边形架构在领域驱动设计和微服务架构设计中扮演了较重要的角色。六边形架构将系统分为内部(内部六边形)和外部,内部代表了应用的业务逻辑,外部代表应用的驱动逻辑、基础设施(诸如 REST,SOAP,NoSQL,SQL,Message Queue 等)或其他应用,UI 层、DB 层、和各种中间件层实际上是没有本质上区别的,都只是数据的输入和输出。内部通过端口和外部系统通信,端口代表了一定协议,以 API 呈现。
一个端口对应多个适配器,对应多个外部系统,对这一类外部系统的归纳,不同的外部系统需要使用不同的适配器,适配器负责对协议进行转换。六边形架构有一个明确的关注点,一开始就强调把重心放在业务逻辑上,外部的驱动逻辑或被驱动逻辑存在可变性、可替换性,依赖具体技术细节。而核心的业务领域相对稳定,体现应用的核心价值。六边形的六并没有实质意义,只是为了留足够的空间放置端口和适配器,一般端口数不会超过 4 个。适配器可以分为 2 类,“主”、“从”适配器,也可称为“驱动者”和“被驱动者”。
代码依赖只能使由外向内。对于驱动者适配器(也称主适配器,Driving Adapter),就是外部依赖内部的。但是对于被驱动者适配器(也称次适配器,Driven Adapter),实际是内部依赖外部,这时需要使用依赖倒置,由驱动者适配器将被驱动者适配器注入到应用内部,这时端口的定义在应用内部,但是实现是由适配器实现。
整洁架构
整洁架构中,同心圆代表应用软件架构的不同部分,也是一种以领域模型为中心的架构,从里到外依次是 Entities、Use Cases、Interface Adapters、Frameworks and Drivers。整洁架构明确了各层的依赖关系,越往里,依赖越低,越抽象,外圆代码依赖只能指向内圆,内圆不知道外圆的任何事情。
洋葱架构
洋葱架构针对六边形架构更进⼀步把内层的业务逻辑分为了DDD概念的应⽤服务层、领域服务层和领域模型层。
特点:
(1)围绕独⽴的领域模型构建应⽤
(2)内层定义接⼝,外层实现接⼝
(3)依赖的⽅向指向圆⼼(注意:洋葱架构提倡不破坏耦合⽅向的依赖都是合理的,外层可以依赖直接内层,也可以依赖更⾥⾯的层)
(4)所有的应⽤代码可以独⽴于基础设施编译和运⾏
总结
我们开发常常会陷入的两个陷阱:
● 看到功能模型后,就开始设计数据模型,考虑数据该怎么创建、怎么更新、什么时候该删除,沦落为CRUD boy。
● 看到功能模型后,就开始考虑操作数据的流程是什么,陷入到事务脚本陷阱。(对于一些简单的功能,不排斥使用事务脚本,但对于复杂功能,事务脚本的维护成本非常大)
而DDD提示了我们不要忙于开发设计,而是需要重视领域模型设计,注重架构的合理性.
image.png












网友评论