前几年就开始接触DDD(Domain Driven Design,领域驱动设计),并且着迷于此。它更多地在战略层指导了我的设计,对于战术层面的设计,目前业界没有统一的标准,也没有特别流行的方案。虽然也有许多技术大牛们热衷于DDD,但一到代码落地便一地鸡毛,造不出“银弹”。
那DDD到底是什么呢?有什么技术落地方案呢?今天我来给大家科普一下。
基本概念
过去系统分析和系统设计都是分离的,正如我们国家“系统分析师” 和“系统设计师” 两种职称考试一样,这样割裂的结果导致,需求分析的结果无法直接进行设计编程,而能够进行编程运行的代码却扭曲需求,导致客户运行软件后才发现很多功能不是自己想要的,而且软件不能快速跟随需求变化。
DDD则打破了这种隔阂,提出了领域模型概念,统一了分析和设计编程,使得软件能够更灵活快速跟随需求变化。
DDD专门为解决复杂性而诞生,因此解决思路完全不同于传统的CRUD,但是DDD本身掌握起来并不会感觉复杂,从程序员角度看,DDD其实是研究将包含业务逻辑的ifelse语句放在哪里的学问。
Q:DDD有什么作用?
A:想想我们做软件设计的初衷是什么?通过微服务与领域驱动设计,简化设计,降低维护成本,提高软件交付速度。这要求我们落地的时候,架设一个支持微服务、支持领域驱动的架构。
Q:DDD适用于什么场景?
A:在Evans写的《领域驱动设计》一书,副标题已经说明一切:软件核心复杂性应对之道。领域驱动的真正作用,在于项目中对日后的维护,当系统变得越来越复杂的时候,才能体现出它的威力。
那新项目该不该用领域驱动?新项目并不复杂,产品还在跑模式的阶段,虽然它的优势并不能真正发挥出来,但我认为,DDD同样适用新项目。项目只会越做越复杂,我们从一开始就应该考虑日渐发胖的代码、随时可能独立的子业务。而新项目更多的是指导战略层设计(如领域建模),战术层的技术落地还是以团队成员最熟悉的方式进行,目标是持续快速交付、降低维护成本。
领域建模
Q:什么是领域模型?
A:为解决场景下的问题而形成的一套模型,然后使用这套模型来解决业务问题。 根据重复劳动经验我们会形成一套模式,领域模型也一样会形成一套模式,包括:实体、值对象、模块、领域服务。
领域发现
那领域模型是怎么一步一步确定下来的呢?推荐两种比较常用的领域发现方法:事件风暴与四色建模法。
一、事件风暴
Event Storming(事件风暴)是一种轻量级的系统分析方法,基于 DDD 的概念,能够为我们梳理系统中的各种相关元素,其中包括了核心的 Aggregate。它能够帮助开发人员梳理核心的业务对象,从某种程度上来说就是就是领域对象中的聚合。
描述产品愿景
产品愿景的主要目的是对产品顶层价值的设计,使产品目标用户、核心价值、差异化竞争点等信息达成一致,避免产品偏离方向。
产品愿景的参与角色:领域专家、业务需求方、产品经理、项目经理和开发经理。
事件风暴关注点
事件:某个动作的结果
属性:事件的输入、输出
命令:某个动作
实体:命令的触发者
简单理解就是谁(实体)使用什么(输入)做了什么(命令、动作)产生了什么(输出)影响了什么(事件)。
二、四色建模法
通过还原业务逻辑事件,依据是否影响公司的运营和发展,确定凭证作为时标型对象,并补全相关描述的建模方法。四色建模法一般运用于问题分析建模。
四色建模是哪四色?它包括——
时标型(Moment-Interval)对象:具有可追溯性的记录运营或管理数据的时刻或时段对象,用粉红色表示;
PPT(Party/Place/Thing)对象:代表参与到流程中的参与方/地点/物,用绿色表示;
角色(Role)对象:在时标型对象与 PPT 对象(通常是参与方)之间参与的角色,用黄色表示;
描述(Description)对象:对 PPT 对象的一种补充描述,用蓝色表示。
分析的五个步骤
找到溯源事件
确定时标型对象
找到周围的PPT对象
找到角色
补全描述对象
四色建模法案例:
领域建模三步曲
对于不同的人提炼出来的领域模型不可能完全一致,这是因为每个人对业务理解的角度都不同。那么,怎么才能保证建模的正确性?
这听起来是个合理的质疑,但实际上却不是那么有道理。首先我们需要明白建模的目的是什么?如果仅仅是为了描画问题,那么并没有什么对错之分——仅仅是立场和角度的差别;而如果是为了企业业务系统而进行建模,那么这个问题应该变为:如何保证模型能够支撑企业的运营?
我给大家梳理以下几个步骤:
一、统一语言,梳理业务
在做设计的时候,梳理业务贯穿了整个过程。这需要技术与业务专家利用统一语言,描绘需求或问题本身,不断梳理业务,提炼出核心的领域模型(而非表设计)。
这有利于拉近技术人员与业务人员之间的关系,建立信任,达成统一的业务目标。
二、识别聚合、聚合根
梳理完业务后,找出实体、值对象,识别出各个聚合与聚合根。
如何识别聚合与聚合根?
一个Bounded Context(限界上下文)可能包含多个聚合,每个聚合都有一个根实体,叫做聚合根;
如何进行关联?聚合根到聚合根:通过ID关联聚合根到其内部的实体,直接对象引用;聚合根到值对象,直接对象引用;对于聚合,有以下设计原则:
聚合是用来封装真正的不变性,而不是简单的将对象组合在一起
聚合应尽量设计的小,尽可能小的拆分,可以避免重构,重新拆分
聚合之间的关联通过ID,而不是对象引用
聚合内强一致性,聚合之间最终一致性
应用层实现跨聚合的调用,避免跨聚合的领域服务调用和数据表关联。
三、划分限界上下文
第二步完成以后,我们根据不同的场景来划分限界上下文,以便进行微服务拆分。通过这种基于业务理解的拆分方式,我们的系统就能做到高内聚,做到单一职责,拆分出来的每个微服务都是软件变化的一个原因,不会因为某个原因发生的变更去修改每个微服务,“牵一发而动全身”。
架构设计
架构设计作为DDD战术层面的设计,关乎到设计如何落地到项目中。下面介绍跨库关联查询解决方案及几种比较流行的架构设计方案。
跨库关联查询解决方案
方案一:数据冗余
这是最简单常用的一种解决方案——以空间换时间,把需要关联查询的条件冗余存储在需要查询的库。举个例子,商品与商品类目被拆到两个独立的服务与数据库中,两者通过类目编码关联,产品想通过类目名称对商品分页查询,这时我们可以把类目名称冗余到商品表存储,给它加个数据库索引即可。
方案二:数据补填
结合Wrapper设计模式,一般在Dao层实现数据聚合——本地库分页查询完数据后,通过查询条件判断是否需要填充关联数据,若需要则通过跨服务查询相关联的服务,再对各个服务的数据进行填充组装,最后返回。
如下图,要实现患者预约查询,并聚合患者、医生数据,则在患者预约服务查询完预约表数据后补填患者服务和医生服务的数据。
这种方法的缺点就是,当一个完整的数据涉及到N个微服务,就会增加N-1个服务调用,数据全量查询/导出的场景也不好使。
CQRS与Event Sourcing
CQRS(Command Query Responsibility Segregation)指的是命令查询职责分离。Command服务专门写数据,使用关系型数据库以保证ACID;Query服务专门读数据,一般使用NoSQL数据库,实现宽表查询,如MongoDB、ElasticSearch等。这是一种索引外置方案。
Event Sourcing事件溯源——简单来说就是,通过事件来管理领域对象的生命周期,事件即领域对象已发生的事实,只增不改。一个对象从创建开始到消亡会经历很多事件,以前我们是在每次对象参与完一个业务动作后把对象的最新状态持久化保存到数据库中,也就是说我们的数据库中的数据是反映了对象的当前最新的状态。
而事件溯源则相反,不是保存对象的最新状态,而是保存这个对象所经历的每个事件,所有的由对象产生的事件会按照时间先后顺序有序的存放在数据库中。可以看出,事件溯源的这种做法是更符合事实观的,因为它完整的描述了对象的整个生命周期过程中所经历的所有事件。那么,事件到底如何影响一个领域对象的状态的呢?很简单,当我们在触发某个领域对象的某个行为时,该领域对象会先产生一个事件,然后该对象自己响应该事件并更新其自己的状态,同时我们还会持久化在该对象上所发生的每一个事件;这样当我们要重新得到该对象的最新状态时,只要先创建一个空的对象,然后将和该对象相关的所有事件按照事件发生先后顺序从先到后再全部应用一遍即可还原得到该对象的最新状态,这个过程就是所谓的事件溯源;另一方面,因为是用事件来表示对象的状态,而事件是只会增加不会修改。这就能让数据库里的表示对象的数据非常稳定,不可能存在DELETE或UPDATE等操作。因为一个事件就是表示一个事实,事实是不能被磨灭或修改的。这种特性可以让领域模型非常稳定,在数据库级别不会产生并发更新同一条数据的问题;
下图为一种支持读写分离的演化:
写服务只有一个统一的Controller入口,通过URL路径与Body传入进入相应的service、方法与入参,然后利用反射定位到具体的Service的某个方法,逻辑处理完后再分发到单Dao,通过配置Vobj.xml映射到对应的表。这样的好处是新增业务只需要写Service与配置映射关系即可,不需要再新增Controller与Dao,Contoller变成了一层薄薄的接入层,大大简化了代码;读服务也有统一的Controller,通过Service分发到不同的DAO层实现,写自定义的SQL或利用ES存宽表来查。
领域驱动架构
与上面的单Dao实现类似,建立统一的Controller与通用的仓库与工厂,利用数据库第三范式实现服务内数据补填,利用服务调用实现跨服务数据补填。
整洁架构
整洁架构的核心思想是通过适配器层解耦业务层与技术框架层代码,使得业务代码与技术框架可以各自升级迭代,互不影响。我们都知道,技术架构一直以来都在不断变化,对项目的技术架构调整成本是非常高的,如何降低这种成本?
这时候整洁架构就派上用场了。
如上图所示,中间的Entities与Use Cases属于业务领域层,Entities表示业务领域模型的核心业务,Use Cases表示与用户交互的Service;最外层技术框架层是各种技术实现,与业务无关的一层;那业务与技术怎么进行关联呢?通过中间绿色的接口适配器层实现。适配器层分离了技术实现与业务逻辑。
下图为整洁架构的一种落地方案。
六边形架构
六边形架构是微服务设计的基础。
如图,我们把微服务封装在六边形里,每个微服务的核心业务是六边形里的应用程序与领域模型。与整洁架构类似,外部接口与内部应用层通过各种适配器进行关联解耦,当发生变更的时候,只需要修改六边形内部即可,不需要修改其它微服务。
清晰架构
清晰架构融合了DDD、整洁架构、CQRS……曾在高水准的平台生产代码中应用,其中一个是拥有数千家遍布全球的网上商店的 SaaS 电子商务平台,另一个是已经在两个国家上线的市场,拥有可以每月处理超过两千万条消息的消息总线。
如上图清晰架构的架构形态,左侧用户从浏览器/客户端/APP等发起业务,经过左侧主适配器,把不同的接入技术与应用层解耦,这样一来业务应用就只需要写一套即可,大大降低了开发成本;数据经过应用层后,通过右侧的从适配器接入,实现不同的存储形式/技术实现,解耦业务代码。
以上给大家介绍了DDD的基本概念、领域建模及几种主流的架构设计方案。DDD是一个非常大的课题,工程师们对DDD的各种争论从不休止,但存在即合理,我们并不一定要把DDD落地到项目中去,在战略层设计可以指导我们准确梳理业务。
无论如何,将来想要成为业务架构师,DDD领域建模与架构设计是一堂必修课,参与到这场思想运动与实践中是非常有必要的。
老规矩,一键三连,日入上千,点赞在看,年薪百万!
本文作者:Jensen
7年Java老兵,小米主题设计师,手机输入法设计师,ProcessOn特邀讲师。
曾涉猎航空、电信、IoT、垂直电商产品研发,现就职于某知名电商企业。
技术公众号【架构师修行录】号主,专注于分享程序员日常、架构技术、职场干货,关注回复“DDD”领取DDD领域建模资料,回复“职场”领取升职加薪资料。
交个朋友,一起成长!