领域驱动设计(DDD)中简单易用的10种技巧

Posted 编程一生

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了领域驱动设计(DDD)中简单易用的10种技巧相关的知识,希望对你有一定的参考价值。

领域驱动设计(DDD),因非常适合与微服务进行配合而闻名,因《领域驱动设计》那本书的难懂而让人望而却步。

其实《领域驱动设计》这本书讲的是:以领域为核心,在代码中体现领域的思想,开发人员和领域专家要紧密沟通。这也侧面给出了公司划分组织结构的建议。

开始之前,我要声明一点:领域驱动设计不是万能药,它适合于实时系统,而对于分析汇总的场景,则不必使用领域驱动设计。

技术是在进步的,现在大家对领域驱动设计抽象的越来越好。今天就来总结一下领域驱动设计简单易用的技巧。首先声明一下:提到的技巧不一定是领域驱动设计提出来的,但是这些技巧可以帮助我们更好的进行领域驱动设计。

限界上下文

画架构图尽量简单易懂。但是项目本身很复杂怎么办呢?那就要按问题域划分子域。每一个子领域架构是很简单的,想看到全貌可以将它们之间的关联形成上下文地图。这就是限界上下文的作用。

DDD建议区分场景来划分子领域,通过依赖接口而不是具体实现解耦。使用微服务实现限界上下文中的低耦合。微服务不是简单的拆分,而是对设计提出了更高的要求。

举个常用的例子:SOA架构中建议进行数据库操作单独提供服务进行,使用方通过接口依赖实现低耦合。这个思想和DDD的思想异曲同工:DDD建议将领域对象与数据库映射,把数据表的增删改查采用接口调用实现。底层数据表结构的修改对上层屏蔽。这就是将软件对象进行持久化操作,并且形成独立的限界上下文。DDD还要求数据库表设计以领域模型为核心,并且推荐不要在sql中进行join操作,而是通过逻辑层来进行,让表与表之间的关系直接转化为软件对象之间的关系。

如果看到这里你还是觉得限界上下文很抽象,不用担心,其他的技巧就是来帮忙咱们划分独立的子领域。

聚合与聚合根

聚合体现的是整体与部分的关系。部分与整体有相同的生命周期。一旦将对象间的关系设计成聚合,外部对象只能访问“聚合根”。充血模型就是一种聚合的实现。

我在《CURD系统怎么做出技术含量--怎样引导面试》里用实例介绍过充血模型。白话来说,就是一个普通的POJO,如:“人”对象,里面有对应属性,如姓、名、年龄、学历、出生年月。贫血模型中“人”对象只包含可以通过lombok自动生成的方法。而充血模型则可以包含由各个属性组成的一个完整简历的方法,或者是直接返回星座的方法。外部只能通过这些方法,也就是“聚合根”来操作“人”这个对象,不能直接访问其内部属性。因为部分和整体有相同的生命周期,意味着人这个对象被销毁,它的属性和方法也同时消失。

DDD工厂与装载

DDD工厂与设计模式中的工厂有较大的差别。

设计模式中的工厂,是将被调用方设计成一个接口下的多个实现,并装配到工厂中。工厂负责通过key值找到对应的实现类,创建出来,返回给调用方。这样就降低了调用方与被调用方的耦合度。

DDD工厂是通过工厂创建领域对象,作为对象生命周期的起点。它的核心在于装配这个环节,比如订单工厂需要将订单对象是要将订单概述、订单明细统一封装到订单对象中。

举个例子:漫画中的超级英雄在设计模式工厂中,工厂创建出来的时候可能是个小菜鸟,在业务逻辑运行过程中,他可能会需要升级打怪才变成超级英雄。而在DDD工厂中,工厂会负责给超级英雄披上钢铁侠的盔甲,装配好美国队长的盾牌。他从工厂里出来就可以拯救世界了。

DDD仓库

DDD工厂和DDD仓库都是聚合的实现。通过DDD工厂装配好的领域对象会返回到DDD仓库中。事实上Spring上下文就是一个DDD仓库底座。它包含了程序需要的各种领域对象。有些领域对象如DAO是从数据库中取的,有些是从缓存中取的,还有些如POJO是直接创建的。但是业务逻辑不关心领域对象的来源,只需要在需要的时候直接使用,就与领域对象的具体实现之间做了解耦。

事件风暴法

在领域设计之初的需求分析阶段,需求分析的基本思路就是统一语言建模。它是我们的指导思想。但落实到实践层面可以采用的方法是事件风暴法。它是一种基于工作坊的DDD实践方法,可以帮助我们发现业务领域中正在发生的事件,指导领域建模以及程序开发。

事件风暴中的风暴是头脑风暴的意思,我在《复联4里用到的方法论》中详细解释过头脑风暴法,这里不再赘述。事件即事实,已经发生,不会更改。信息管理系统可以将这些事实以信息的形式存储,即信息就是事实。信息管理系统的作用就是将信息存储,管理与跟踪。对事件可以采用上游发布、下游订阅的方式进行解耦,按照业务流程来梳理领域事件。

举个例子:把大象装冰箱分成三步,每个步骤都是一个事件,每个事件之间要解耦。打开冰箱门涉及冰箱领域对象、人领域对象等。这个事件完成将会触发一个信息存储:冰箱状态变成已打开。这些都是围绕着打开冰箱门这个事件展开。这个事件完成后,只要对第二步把大象装冰箱进行通知即可。之后的事情再不会和第一步发生关系。

防腐层

防腐层(Anti-corruption layer,简称 ACL)介于新逻辑和旧逻辑之间,用于确保新逻辑的设计不受老逻辑的限制。是一种在不同逻辑间转换的机制。

创建一个防腐层,以根据客户端自己的域模型为客户提供功能。该层通过其现有接口与新逻辑进行通信,几乎不需要对其进行任何修改。因此,防腐层隔离不仅是为了保护你的系统免受异常代码的侵害,还在于分离不同的域并确保它们在将来保持分离。

举个例子:我之前带过的项目,由于架构不能满足业务增长的需求,需要进行大范围重构。这里我们使用了“留壳扣瓤”的方法,对外接口保持不变,内部实现换成新的,这个接口层就是我们的防腐层。当然啦,不要以为瓤随随便便就可以换了,过渡期我们保留了旧逻辑,用开关的方式在新老逻辑之间做灰度和切换,并准备好随时降级。

谦卑对象模式

谦卑对象模式是整洁架构中提出的一个概念。最初的设计目的是帮助单元测试的编写者区分容易测试的行为与难以测试的行为。难以测试的行为即谦卑对象,要尽量的简单,因为这些难以通过测试保证其正确性。

例如:GUI是很难进行单元测试的。可以利用谦卑对象模式将GUI的这两种行为拆分成视图和数据两部分。视图部分属于难以测试的谦卑对象,这种对象的代码应该越简单越好。

再比如:数据库网关接口的实现,属于谦卑对象。所以我们使用成熟的框架,自身的代码尽量简单。测试时我们只需要对网关接口进行Mock,测试其他逻辑。网关究竟是怎么与数据库进行交互的,往往不在单元测试中考虑。

谦卑对象模式与DDD有什么关系呢?近年来,高可用、稳定性、弹力设计、容错设计和业务连续性越来越得到公司的重视。基于稳定性的隔离术也逐渐成为划分问题域的考虑方向。我在《服务的容灾与容错》这篇文章中总结了隔离术,这里不再赘述。

总结

DDD本质就是利用分治的思想将复杂的事情简单化,本文所讲的技巧就是按什么来分治的问题。本文涉及的概念整体结构如下图:

领域驱动设计(DDD)分层架构的三种模式

模式一:四层架构

1.User Interface为用户界面层(或表示层),负责向用户显示信息和解释用户命令。这里指的用户可以是另一个计算机系统,不一定是使用用户界面的人。
2.Application为应用层,定义软件要完成的任务,并且指挥表达领域概念的对象来解决问题。这一层所负责的工作对业务来说意义重大,也是与其它系统的应用层进行交互的必要渠道。应用层要尽量简单,不包含业务规则或者知识,而只为下一层中的领域对象协调任务,分配工作,使它们互相协作。它没有反映业务情况的状态,但是却可以具有另外一种状态,为用户或程序显示某个任务的进度。
3.Domain为领域层(或模型层),负责表达业务概念,业务状态信息以及业务规则。尽管保存业务状态的技术细节是由基础设施层实现的,但是反映业务情况的状态是由本层控制并且使用的。领域层是业务软件的核心,领域模型位于这一层。
4.Infrastructure层为基础实施层,向其他层提供通用的技术能力:为应用层传递消息,为领域层提供持久化机制,为用户界面层绘制屏幕组件,等等。基础设施层还能够通过架构框架来支持四个层次间的交互模式。

模式二:五层架构

一、三层架构(Data、Context和Interactive)
Data层描述系统有哪些领域概念及其之间的关系,该层专注于领域对象的确立和这些对象的生命周期管理及关系,让程序员站在对象的角度思考系统,从而让“系统是什么”更容易被理解。
Context层:是尽可能薄的一层。Context往往被实现得无状态,只是找到合适的role,让role交互起来完成业务逻辑即可。但是简单并不代表不重要,显示化context层正是为人去理解软件业务流程提供切入点和主线。
Interactive层主要体现在对role的建模,role是每个context中复杂的业务逻辑的真正执行者,体现“系统做什么”。role所做的是对行为进行建模,它联接了context和领域对象。由于系统的行为是复杂且多变的,role使得系统将稳定的领域模型层和多变的系统行为层进行了分离,由role专注于对系统行为进行建模。该层往往关注于系统的可扩展性,更加贴近于软件工程实践,在面向对象中更多的是以类的视角进行思考设计。
二、五层架构
1.User Interface是用户接口层,主要用于处理用户发送的Restful请求和解析用户输入的配置文件等,并将信息传递给Application层的接口。
2.Application层是应用层,负责多进程管理及调度、多线程管理及调度、多协程调度和维护业务实例的状态模型。当调度层收到用户接口层的请求后,委托Context层与本次业务相关的上下文进行处理。
3.Context是环境层,以上下文为单位,将Domain层的领域对象cast成合适的role,让role交互起来完成业务逻辑。
4.Domain层是领域层,定义领域模型,不仅包括领域对象及其之间关系的建模,还包括对象的角色role的显式建模。
5.Infrastructure层是基础实施层,为其他层提供通用的技术能力:业务平台,编程框架,持久化机制,消息机制,第三方库的封装,通用算法,等等。
三、六层架构
1.User Interface是用户接口层,主要用于处理用户发送的Restful请求和解析用户输入的配置文件等,并将信息传递给Scheduler层的接口。
2.Scheduler是调度层,负责多进程管理及调度、多线程管理及调度、多协程调度和维护业务实例的状态模型。当调度层收到用户接口层的请求后,委托Transaction层与本次操作相关的事务进行处理。
3.Transaction是事务层,对应一个业务流程,比如UE Attach,将多个同步消息或异步消息的处理序列组合成一个事务,而且在大多场景下,都有选择结构。万一事务执行失败,则立即进行回滚。当事务层收到调度层的请求后,委托Context层的Action进行处理,常常还伴随使用Context层的Specification(谓词)进行Action的选择。
4.Context是环境层,以Action为单位,处理一条同步消息或异步消息,将Domain层的领域对象cast成合适的role,让role交互起来完成业务逻辑。环境层通常也包括Specification的实现,即通过Domain层的知识去完成一个条件判断。
5.Domain层是领域层,定义领域模型,不仅包括领域对象及其之间关系的建模,还包括对象的角色role的显式建模。
6.Infrastructure层是基础实施层,为其他层提供通用的技术能力:业务平台,编程框架,持久化机制,消息机制,第三方库的封装,通用算法,等等。

模式三:六边形架构

有一种方法可以改进分层架构,即依赖倒置原则(Dependency Inversion Principle, DIP),它通过改变不同层之间的依赖关系达到改进目的。
根据该定义,DDD分层架构中的低层组件应该依赖于高层组件提供的接口,即无论高层还是低层都依赖于抽象,整个分层架构好像被推平了。如果我们把分层架构推平,再向其中加入一些对称性,就会出现一种具有对称性特征的架构风格,即六边形架构。六边形架构是Alistair Cockburn在2005年提出的,在这种架构中,不同的客户通过“平等”的方式与系统交互。需要新的客户吗?不是问题。只需要添加一个新的适配器将客户输入转化成能被系统API所理解的参数就行。同时,对于每种特定的输出,都有一个新建的适配器负责完成相应的转化功能。

六边形架构也称为端口与适配器,如下图所示:
技术图片

六边形每条不同的边代表了不同类型的端口,端口要么处理输入,要么处理输出。对于每种外界类型,都有一个适配器与之对应,外界通过应用层API与内部进行交互。上图中有3个客户请求均抵达相同的输入端口(适配器A、B和C),另一个客户请求使用了适配器D。假设前3个请求使用了HTTP协议(浏览器、REST和SOAP等),而后一个请求使用了AMQP协议(比如RabbitMQ)。端口并没有明确的定义,它是一个非常灵活的概念。无论采用哪种方式对端口进行划分,当客户请求到达时,都应该有相应的适配器对输入进行转化,然后端口将调用应用程序的某个操作或者向应用程序发送一个事件,控制权由此交给内部区域。
应用程序通过公共API接收客户请求,使用领域模型来处理请求。我们可以将DDD战术设计的建模元素Repository(存储库)的实现看作是持久化适配器,该适配器用于访问先前存储的聚合实例或者保存新的聚合实例。正如图中的适配器E、F和G所展示的,我们可以通过不同的方式实现资源库,比如关系型数据库、基于文档的存储、分布式缓存或内存存储等。如果应用程序向外界发送领域事件消息,我们将使用适配器H进行处理。该适配器处理消息输出,而上面提到的处理AMQP消息的适配器则是处理消息输入的,因此应该使用不同的端口。
我们在实际的项目开发中,不同层的组件可以同时开发。当一个组件的功能明确后,就可以立即启动开发。由于该组件的用户有多个,并且这些用户的侧重点不同,所以需要提供多个不同的接口。同时,这些用户的认识也是不断深入的,可能会多次重构相关的接口。于是,组件的多个用户经常会找组件的开发者讨论这些问题,无形中降低了组件的开发效率。
我们换一种方式,组件的开发者在明确了组件的功能后就专注于功能的开发,确保功能稳定和高效。组件的用户自己定义组件的接口(端口),然后基于接口写测试,并不断演进接口。在跨层集成测试时,由组件开发者或用户再开发一个适配器就可以了。

以上是关于领域驱动设计(DDD)中简单易用的10种技巧的主要内容,如果未能解决你的问题,请参考以下文章

领域驱动设计:领域子域和限界上下文

领域驱动设计 - 战略设计 - 1/2限界上下文

DDD领域驱动设计-DDD概览

领域驱动设计 DDD的一些基础概念

DDD领域驱动设计实战-聚合(Aggregate)和聚合根(AggregateRoot)

DDD(领域驱动设计)从入门到精通