最近几年,DDD(Domain-Driven Design)领域驱动设计越来越受技术人重视,不论是传统企业数字化转型项目中,还是平时的软件架构设计中,抑或是各种技术文章中,DDD都是讨论的热点。特别是微服务架构和云原生技术时代之后,DDD对于构建统一语言、系统应用架构设计、微服务划分等都是非常重要的手段。今天我们就来看一看DDD的一些通用知识、核心设计方法、分层架构、常见的设计原则、以及一些通用的例子。
DDD是什么
DDD是Eric Evans在2003年出版的《领域驱动设计:软件核心复杂性应对之道》(Domain-Driven Design: Tackling Complexity in the Heart of Software)一书中提出的具有划时代意义的重要概念。DDD通过统一语言、领域模型、领域划分等一系列手段来降低软件复杂度。
大家对面向对象设计(OO)不会陌生,其实DDD就是在面相对象设计基础上的扩展和延伸,DDD 基于面向对象分析技术进行了分层规划,对软件开发过程全生命周期使用语言的统一,并强调业务与技术相结合的一种过程。DDD利用面向对象的特性(封装、多态)有效地化解复杂性,相比之下,传统的数据驱动设计(比如基于J2EE或事务性编程模型)只关系数据,除了简单的setter/getter方法外,不包含业务逻辑,业务逻辑都是以过程式的代码写在Service中。这种方式极易上手,但随着业务的发展,系统也很容易变得混乱复杂。
我们先来看一个例子,Eric Evans在书中有一个货物运输的例子,经过一系列分析和讨论,最后得到的领域模型如下:
1. 一个Cargo(货物)涉及多个Customer(客户,如托运人、收货人、付款人),每个Customer承担不同的角色;
2. Cargo的运送目标已指定,即Cargo有一个运送目标;
3. 由一系列满足Specification(规格)的Carrier Movement(运输动作)来完成运输目标。
从中我们可以看出,Eric没有从用户的角度去描述领域模型,而是以领域内的相关事物为出发点,考虑这些事物的本质关联及其变化规律的。比如,他将货物作为整个领域的中心,而客户等角色都是围绕其周围的角色;同时货物有一个确定的目标,经过一系列的运输动作到达目的地。而一般我们基于过程或者基于数据为中心,往往只停留在分析的表面,而领域驱动强调对核心的领域进行建模,挖掘需求的本质。
DDD vs数据驱动设计
我们在进一步看看DDD与传统数据驱动的不同,这一点往往技术同学不太关注。一般情况下,很多同学喜欢从数据出发,先梳理ER图,设计数据库表,编写DAO,然后进行业务实现。数据驱动主要是贫血模型,业务逻辑散落在大量的方法中。数据模型设计关注的是数据存储,数据尽量不要冗余,控制表数量不膨胀,并重视数据的扩展性。这样的设计不是不可以,系统小规模时还好,当系统越来越复杂时,开发时间指数增长,维护成本很高。
而领域驱动设计从领域出发,分析领域内模型及其关系,并进行领域建模,设计核心业务逻辑,进而再进行技术细节实现。DDD 优先考虑领域概念的业务语义表达,具有独立业务概念的东西会尽量抽象成一个内聚的领域对象。领域对象不仅仅有属性,还有该有的行为。其核心思想是通过领域驱动设计方法定义领域模型,从而确定业务和应用边界,保证业务模型与代码模型的一致性。
在DDD中,领域模型和数据模型是解耦的,以领域模型为出发点,进一步体现到核心的实体、值对象、聚合、领域服务上,强调对业务语义的显性化表达,而不是数据的存储和数据之间的关系,这是“领域驱动设计”和“数据驱动设计”之间显著的区别。在软件工程的早期,为了弥补二者之间的差异,先驱者们尝试用(Object Relationship Mapping,ORM)工具,但工具是无法进行映射的,而DDD的核心并不是用什么工具,而是如何整体的建模分析的过程。所以,大家在进行DDD时,一定要跳出数据模型优先的束缚,不要让领域模型被数据模型绑架。
DDD的好处
DDD有非常多的好处,这里我重点强调其中2点:
+ 面向对象:DDD是面向对象的扩展,很多面向对象的概念比如封装、多态,以及核心的设计原则比如SOLID都可以在DDD中有非常好的应用。可以说是对我们整体应用架构设计的重要指导,比如如何建模、如何划分边界、如何定义微服务的粒度等。
+ 统一语言:团队成员会在有界的上下文中有意识的形成统一语言,并使得大家在同一平面讨论问题,业务复杂度与技术复杂度分离,然后将这些概念设计成一个领域模型;DDD不以人为中心,而是以业务为中心,可以更好地构建业务领域知识,进而反哺业务相应能力提升。
如何进行DDD
DDD 包含战略设计、战术设计、技术实现三个部分。战略设计侧重于高层次、宏观上去划分领域和限界上下文等,而战术设计则关注使用DDD的核心概念,比如实体、值对象、领域服务、聚合、领域事件等来细化上下文,通过领域模型来表达业务。技术实现主要通过分层架构来隔离领域模型代表的业务逻辑和技术细节。
DDD战略设计
战略设计主要设计如下几个核心概念:通用语言、领域、限界上下文等。
(1)通用语言 Ubiquitous Language
技术同学经常用DAO、DTO等技术视角来对分类对象,而这与业务视角讨论的业务术语经常对不起来,导致彼此互相听不懂,沟通效率低。DDD通过一套面向对象的分类方法,从领域出发,实现软件开发过程中各个角色和环境的“统一语言”。通用语言建立的过程并非容易,因为技术人员和领域专家在沟通过程中存在天然屏障的,过程中需要各方充分沟通。
(2)领域 Domain
领域是来确定范围和边界的,同时为了降低业务理解和系统实现的复杂度,DDD会将领域进一步划分成更小粒度,也就是子域。所以,领域可以进一步分为核心域、通用子域、支持子域等概念。领域中的核心是领域模型(Domain Model),领域模型通过提炼领域对象,定义领域对象之间的关系,属性和行为,属于DDD的核心产物。
(3)限界上下文 Bounded Context
领域帮助我们对系统进行拆分,而限界上下文帮助回答域之间的边界以及如何交互,限界上下文是一个显式的概念性边界,领域模型都存在于这个边界之内,出了这个边界就不能确保这个含义。DDD中有一个对限界上下文的形象比喻,“细胞之所以会存在,是因为细胞膜定义了什么在细胞内、什么在细胞外,并且确定了什么物质可以通过细胞膜。” 同时,限界上下文的交互方法有多种,在实际工作中,目前用的比较广的是防腐层和统一协议。
这里就领域和限界上下文做一个简单例子,比如一个购物车订单支付下单的例子,购物车进行在线的支付授权,订单处理下单过程,并触发支付域的付款结算。这里我们简化整个建模的过程,假设已经抽象出购物车域、支付域、订单域(当然通常购物车也可以包含在订单域),领域内部我们进行了核心模型的聚合,图中只展示了核心的Cart、Payment、Order。领域之间我们通过限界上下文进行交互,因为购物车域支付域密切相关,需要等待支付授权,我们通过防腐层ACL进行关联;而订单下单和付款动作相对接耦,我们通过领域事件(后文会介绍),在订单已下单后,触发支付的付款动作。
DDD战术设计
DDD战术层面是领域建模最核心的一步,是分析实体、值对象、领域服务、聚合、领域事件等核心概念。
(1)实体 Entity
实体是一个具有唯一身份标识的对象,并且可以在相当长的一段时间内持续地变化。另外,实体具有可变性,内部一般是充血模型,会封装包含这个实体相关的业务逻辑。以订单Order为例,Order有订单实体,下单、发货和退单等行为,但传统经典方式是将这些行为放到另一个服务OrderService中,而不是Order对象之中。
(2)值对象 Value Object
值对象是只关心属性的对象,没有标识符。值对象一般依附于实体而存在, 是实体属性的一部分,而非独立存在,是不变的。值对象没有唯一标识,值对象功能单一,一般是贫血模型。以Order为例,订单下的送货地址Address就是典型的值对象。Address并不是随着Order产生而产生,相对不变,也不需要一个单独标示。
(3)领域服务 Domain Service
领域中的一些概念不太适合建模为对象,它们本质上是一些操作,一些动作,往往会涉及到多个领域对象,这就是领域服务。识别领域服务有几个关键特征:领域服务体现的行为不属于任何实体和值对象的;被执行的操作涉及到领域中的其他的对;操作是无状态的。以OrderDelivery订单发货为例,需要Order和履约两种实体之间通过一定的业务逻辑,并确保事务,可以作为DomainService。
(4)聚合 Aggregate
聚合的组成由两部分,一部分称为根实体,是聚合中的特定实体;另一部分描述一个边界,定义聚合内部都有什么。一个域的聚合可以理解成整个域中核心实体和值对象的组合,并通过一个根实体进行代表,同时要满足固定规则。比如订单域可能有很多实体,如订单、子订单、订单明细、地址、物流信息、支付信息等,而我们聚合为订单域后,这些实体都聚焦在一起,并由Order这个实体作为聚合根对外交互。
(5)领域事件 Domain Event
领域事件表示领域中所发生的重要事件,事件发生后通常会导致进一步的业务操作,或者在系统其它地方引起反应。领域事件非常重要,我们系统设计过程中经常需要接耦,技术同学一般通过MQ方式进行;架构同学可能使用Event-Driven Architecture EDA的方式,而Serverless架构中核心的就是基于事件编程,这一切的核心就是对领域事件的设计,不过当前大部分系统Event设计比较随性,也导致Event滥用和无用情况发生,而领域事件是对我们很好的方向指引。比如订单例子中,订单已下单后,会触发库存冻结、支付状态更新、物流同步等,都是对系统事件的良好接耦设计。
下面通过一张图就前面提到的一些核心概念做一个小的总结:
DDD常用分析方法
DDD一些常用的分析方法主要有用例分析法,四色建模法,以及事件风暴法。这里我们介绍两种方法。
(1)用例分析法
用例分析是比较通用的领域建模方法,可以在比较传统需求调研过程中再结合领域模型的设计思路进行,核心是通过业务需求、场景流程等梳理用例,进而规划领域模型。编写用例时要避免使用技术术语,而应该用最终用户或者领域专家的语言。进而,我们可以基于用例的方法,根据语义来整理用例,进而整理领域模型,大体可以按照如下方式:
+ 收集用例
+ 提取实体
+ 提取属性
+ 添加关联
+ 完善模型
举个例子,比如让你设计一个电商客服系统,一个典型的User Story可能是“客户A反馈订单问题,客服说你留个电话,有进一步处理结果我会通知你”,这里
· 客户A是客户。
· 电话是客户的属性。
· 客服包含了客服系统,客服员工两个关键对象。
· 订单、工单肯定也是关键领域对象;
· 通知这个动词暗示可能有领域事件,或技术上通过观察者模式实现。
· 进而梳理之间的关系,比如一个客户可能有多个工单,是O2M关系等。
(2)事件风暴法
前文我们提到了领域事件,而基于领域事件的建模方法就是事件风暴。事件风暴与头脑风暴类似,可以快速分析复杂业务领域,完成领域建模的目标。事件风暴是事件驱动设计的典型代表,是一种群体建模技术。关注如下元素:
+ 事件:发生了什么事情,产生了什么结果,用桔黄色表示。
+ 属性:事件的输入、输出,是对时间的细化描述
+ 命令:某个动作的发起者,可能是人,外部事件,定时器等,用蓝色表示。
+ 领域:领域的聚合,内聚,低耦合,聚合内保证数据一致性,用黄色表示。
简单理解就是谁在何时基于什么(输入)做了什么(命令)产生了什么(输出)影响了什么(事件),最后聚合成怎样的(领域)。前文我们提到了“订单已下单”这种事件,这里主要提供一下关键图例。
举个例子,比如角色“用户”进行了命令“提交订单” 产生事件“订单已创建”,或者角色“运营人员”进行了命令“同步库存” 产生事件“库存已变化”。
对于事件Event,也有一些注意事项:
+ Event命名:Domain Name + 动词的过去式 + Event。比如OrderCreatedEvent
+ Event内容:Enrichment(payload中放data),Query-Back(通过回调拿到更多的data)
+ Event管理:通过MQ等保存所有的Events,并提供良好的好的Event查询和回溯。
+ Event处理:事件构建和发布、事件数据持久化、事件总线、消息中间件、事件接收和处理等。
DDD技术实现-分层架构
DDD在具体落地实时过程中,强调四层分层结构,将前面提到的核心概念进行有效的整合,各层的职能定义如下:
+ 展示层:负责向前台显示信息和解释用户命令,完成前端界面逻辑。展示层的组件实现用户与应用交互的功能,也可以叫用户接口层,面向前端应用,提供应用级别入口完成用户操作。比如对前端展示(web,wireless,wap)的路由和适配,也相当于MVC中的controller。
+ 应用层:负责展现层与领域层之间的协调,负责获取输入,组装上下文,参数校验,调用领域层做业务处理,对领域层组件进行简单封装,例如事务、执行单位操作、调用应用程序的任务。
+ 领域层:是领域驱动设计的核心,包含了前面提到的核心概念,如领域实体、值对象、领域服务、聚合以及它们之间的关系,负责表达业务概念、业务状态信息以及业务规则,具体表现形式就是领域模型。
+ 基础设施层:向其他层提供通用的技术能力,为应用层传递消息(API 网关等),为领域层提供持久化机制(如数据库资源、中间件交互、缓存、MQ消息、搜索引擎、文件系统)等,屏蔽技术底座能力以及其他通用的工具类服务。
除了比较经典的4层分层架构,DDD还有一种松散分层理念,认为应该推平分层架构,平面型分层得以出现。平面型分层架构通过划分内部和外部,系统由内而外围绕领域模型进行展开,领域部分位于最内层,应用程序包含领域模型和业务逻辑,对于外部而言,通过各种适配器进行上下文集成,包括数据持久化、APP、Web应用、第三方数据集成等。这样很好地做到了业务逻辑和用户界面的交错问题,实现了前后端隔离,依赖关系明确,由外向内依赖。
当然,工具层面,也有一些比较好的开源框架,用来支持DDD分层架构的相关脚手架产出,因为DDD的核心是缄默分析的过程,对于工具,每个工具定位不同,同时大家在具体的项目中DDD的目的也有不同,这里我们不做推荐介绍,也欢迎感兴趣的同学留言和讨论。
DDD设计的几点建议
1、设计原则:尽可能follow SOLID原则,即单一职责原则(SRP),开闭原则(OCP),里氏替换原则(LSP),接口隔离原则(ISP),依赖倒置原则(DIP),以及23个设计模式。
2、关注依赖问题:比如无循环依赖、依赖倒置等。同时,注意一些常用的服务设计原则,比如服务无状态、重试幂等性。
3、重视建模本身:DDD不仅是一种编程语言,也是一种思维模式,核心是业务需求理解,统一语言建模,不要过于纠结用什么工具和具体框架,适合自己的才是最好的,同时基于六边形框架设计,做到框架无关。
4、模型数量:实体、服务、事件等并不是越多越好,要注意核心模型的复用,逻辑内聚,复用性高,减少接口数量。同时尽量减少跨域的调用,做好限界上下文的处理。
5、DDD并不是万能的:如果系统并没有复杂的业务逻辑,可以用一般的面向数据的架构或者事务脚本等模式即可,杀鸡不用牛刀。DDD的学习、改造和兼容成本需要我们关注。
6、事务:DDD的聚合中有描述事务,一个事务内只操作一个聚合实例。建议最终一致性,并在应用层声明事务。
7、CQRS: Command Query Responsibility Segregation用于将领域模型与查询功能进行分离,让一些复杂的查询摆脱领域模型的限制,以更为简单的 DTO 形式从DB中直接向前台展现查询结果,分离不同的数据存储结构,让开发者按照查询的功能与要求更加自由的选择数据存储引擎,比如通过异步处理、大数据搜索等方式。
8、服务编排:DDD是非常重要的应用架构设计方法,对于业务架构,还需要进一步的梳理业务流程和业务能力,可能会涉及到流程进一步编排,这就要做好架构的分层,让DDD更加发挥自己的特长,让更适合做流程编排的比如流程引擎来做服务编排。
9、微服务与中台:DDD是微服务和中台中非常重要的分析方法,微服务进一步通过技术进行落地,而中台是更上层的全局架构设计思想,互为补充。
电商DDD例子
前文中,我们介绍了不少DDD在战略、战术、技术层面的特性,这里我们来综合看一个例子。这是一个简单的电商例子,只展示了非常核心的几个领域,每个领域也只挑选了个别领域实体和值对象,主要给大家一个大体的全局感觉。
+ 领域:用户域、会员域、营销域、订单域、商品域、库存域,这里没有画全,比如还会有支付、结算、物流、履约等。
+ 聚合:这里强调一下聚合根,比如会员域的会员,营销域的活动人群,用户域的账号等。
+ 领域模型:图中白色的框框,不同域各不相同,比如商品中需要进一步细化处SKU、SPU。
+ 值对象:图中灰色的框框,比如商品域中的属性、规格、价格。
+ 领域服务:比如会员创建用户,商品购物车关联,会员参与活动等。
+ 领域事件:比如会员商品已添加,商品库存已同步等。
需要注意的是,在真正的DDD实践中,会远远大于上图中的内容,比如每个域的分析构建都需要大量的设计细节(比如会员域和支付域,大家可以参考我公众号单独的文章),其中一些领域事件、限界上下文也并没有画的非常规范。但我认为DDD最大的魅力正是分析梳理的过程,也希望读者朋友,自己动手画一画你认为的电商核心领域模型,或者你所在的业务行业的领域模型,与我交流。如果大家想进一步了解DDD,也建议大家再仔细阅读一下《领域驱动设计:软件核心复杂性应对之道》《实现领域驱动设计》
小结
DDD是我们进行系统建模分析的利器,好处有很多,比如统一语言、业务领域知识沉淀、边界清晰设计、关注点分离等。DDD通过限界上下文、值对象、聚合、领域服务、领域事件等核心概念,根据用例分析、事件风暴等方法进行设计,并基于分层架构进行技术落地开发。当然DDD不是万能的,有较高的学习门槛,需要整个团队形成统一认识,也需要相应的规范和技术落地。因此DDD在使用时要时刻注意选择它的初衷,不用局限在相关的原则等条条框框,如果DDD的相关知识框架对大家在模型构建和相互协同中有一定帮助,我认为就非常有意义。
作者简介
王思轩,博士
北美计算机博士,哈工大本硕,7年北美和欧洲海外经历,10余篇国际学术论文。曾任阿里云云原生解决方案架构师,多年从事于云计算和架构设计工作。致力于数字化转型、架构理论、中台、云原生、新零售技术、技术产品化、领域建模、全球化技术等领域。
• end •
作者 | 王思轩
架构思轩
架构思考的小站