架构师之路-领域驱动设计DDD
引文
领域驱动设计(Domain-Driven-Design),简称 DDD,它是一种基于专业领域知识,去解决复杂的业务问题的软件开发方法论。
领域驱动的设计理念是以客户与产品为导向,进行业务拆分的一套架构设计思路,其实DDD的概念并不是最近才出现的,它早在2003年的时候就被提出了,但是一直没有得到太多的关注,直到最近几年,DDD的概念才深入人心,因为它随着微服务的出现而变得尤为重要,为什么呢? 因为微服务更偏重于技术架构,要求技术架构能适应市场以及业务的快速变化,但我们在运用微服务的实际情况是,给微服务带来变化的不是外在的市场,基本都是前期沟通不充分导致的。
原则
领域驱动的核心原则主要由以下两点
- 项目的重点一定是领域本身,而不是技术细节
- 复杂的领域设计应基于模型
极限编程
极限编程(Extreme Programming)我们称之为Xp编程,它是一门针对业务和软件开发的规则,它的作用在于将两者的力量集中在共同的、可以达到的目标上。它是以符合客户需要的软件为目标而产生的一种方法论,XP使开发者能够更有效的响应客户的需求变化,哪怕是在软件生命周期的后期。
DDD领域驱动设计是XP编程的一种,我们常见的Xp编程还有TD,TDD,BDD,ATDD,本博文就简单介绍下TD与TDD,了解下极限编程的概念
传统模式开发(TD)
传统开发模式(Traditional Development) 简称 TD: 传统开发模式,就是开发人员拿到业务需求后,进行设计,设计完成后开始书写代码,最后写测试用例进行测试。这种模式的优点是开发迅速,交付快,缺点也很明显,中途出问题(例如业务逻辑有问题等)可能白写。
测试驱动开发(TDD)
测试驱动开发(Test-Driven Development) 简称 TDD: 它的特点就是自动测试先行。它的过程就是由研发人员在拿到需求以后,先根据需求去写测试用例,然后编写测试代码通过测试,最后在不改变测试用例整体行为时重构为正是代码。与传统方式不同点在于,他不去进行设计,靠一边写测试用例,一边进行构造设计。
优点:
- 关注业务,不关注实现,代码清晰,流程清晰
- 编写测试用例能够体现发现业务需求带来的问题
- 交付时bug相比之下会少很多
缺点:
- 二次重写,近乎相当于写两次代码
- 维护困难,业务需求变更等于再写两遍(测试跟正式)
- 全局设计匮乏,一边写一边设计,可能只在乎当前步骤的功能点,而忽视了全局。
通用语言
产生问题的根源
在TD跟TDD的介绍后,有没有发现一个问题,问题点就是业务干业务的,业务干完了,开发开始干,有问题再去找业务,这个看似协作的亲密无间,实则慢慢互相仇恨。那么导致这些问题产生的问题点在哪呢?大致可以分为如下几块
- 业务团队在讨论业务模型时,往往无视技术团队,最后的结果可能就是,你了解下需求开发即可。
- 相对的,技术团队也往往无视业务的领域专家参与技术团队的架构模型,因为业务往往对于技术团队的领域架构模型不甚了解。
- 缺少通用语言,技术与业务沟通困难。
统一语言
统一语言(Ubiquitous Language) 是DDD的核心之一,在我们与业务或产品的交流过程当中,往往是两个世界的人,最经典的案例,莫过于…
- 业务:“我想我们的APP有一个功能,他能随着用户手机壳的颜色变化而变化”
- 开发此时心里咒骂一万遍,回道:“这是个不可能的需求”
当然,这个案例可能比较夸张,但是产生这样的问题点在于哪呢? 问题点就在于,作为开发人员,是从技术的角度上,而且更多的是说一些专业的术语, 领域专家则是以软件项目,用户体验的角度上进行诉说。在这个基础上交流,每个人都会按照自己的理解去理解对方的话语,就会导致结果,跟我们以前玩过的一个语言传递游戏一样,第一个人在描述"我摔倒了",经过了几个人模仿变更,最后一个人说的时候变成了"我在上厕所"。
举个典型例子,一个电商系统的业务组织项目组开会,这里面有3个开发人员,一个管消息服务,一个管买家服务,一个管卖家服务,此时业务开会说了句,“用户体验感极差”,这是,消息服务人员在想:“客服与客户之间交流体验感不好吗?” 买家服务开发人员在想:“游客是对商品展示是不够友好吗”,卖家服务的开发人员在想:“商家在哪个功能体验不好吗?” 。出现了什么问题,一个用户,出现了至少3种以上的不同理解。
所以,我们要制定领域模型,将整个领域模型进行合理拆分(具体如何拆分,在后文中会讲),是拆分后的每个单词都有独特的含义,而且被开发人员,架构师,领域专家所认可,这个就可以作为在这个企业,这个项目唯一的通用语言被记录。
统一语言的优势
我们了解了统一语言的制定规则,那统一语言可以给我们带来什么样的好处呢?
-
统一协作
在需求立项时,由业务专家,架构师跟开发人员一起沟通,不仅能提高沟通质量,减少业务与开发的沟通障碍,更能让业务、产品、研发人员成为一个团队在工作,而不是各司其职。
-
需求的准确性
业务人员在撰写业务文档时,未必能够考虑的面面俱到,这些缺陷往往需要研发人员指出。
-
编码与设计的一致性
开发人员直接参与到业务需求的讨论中,可以更明确自己的编码过程与代码设计,使领域驱动的方法论效率最大化。
-
价值回归
DDD让研发跟业务人员目标一致,避免技术上的求高求新,回归对业务实现的目标上来。
-
交付质量高
前期充分的沟通与讨论,可以大大的减少程序交付带来的Bug数量。
领域驱动四层架构
分层的价值在于每一层都只代表程序中的某一特定方面,这种限制能使每个方面的设计都更具内聚性。现在我们就说说领域驱动的核心四层:
- 展现接口层: 我们熟知的
Controller
,它定义着软件项目要整体完成的项目,与应用层交互的必要通道。 - 应用层: 应用层不包含业务规则,它为下一层的领域层对象进行协调任务,分配工作,是他们相互协作,他虽然不包含业务信息,但是可以知道业务走到哪一步了。
- 领域层(领域模型): 这是我们要关心的,也是领域驱动业务的核心,他负责着业务逻辑的整体流程,细节,以及对后续基础设施层的实现
- 基础设施层: 为上层提供基础服务的能力,例如持久化数据,发送邮件等。
战略建模
领域 (Domain)
领域抽象的可以理解成你要做的业务系统是哪种类型的。比如,你要开发一套仓储管理系统,那么仓储管理范围之内的业务,就是一个领域,也可以称之为仓储管理领域,但是领域有一个明确的界限,不能超出以划分领域的这个范围,比如仓储之上可能存在供应链,那供应链相关的业务就不属于仓储管理领域。相对的,一个仓储管理领域,也可以拆分为各个子域,比如仓库管理领域,库存管理领域,用户管理领域,它们是对领域功能的支撑同样子域之间的交互是通过上下文,也就是限界上下文,它是子域中域与域之间沟通的桥梁。
子域 (Sub Domain)
根据上图,可以看出,子域又可细分为核心域、支撑子域与通用子域。
- 核心子域 (core): 核心子域,阐述为子域的核心,即为构成领域的核心域,比如仓储管理领域中,库存管理域,仓库管理域就是它的核心域。
- 支撑子域 (support): 即为支撑核心子域的支撑域,比如仓库管理域中,你可能会用到地图来定位分布在全国各地的仓库,然后根据算法进行调剂功能等业务,那么地理位置管理域即为仓库管理域的子域,因为在仓储管理领域中,只有地理位置管理域没有任何价值,它去服务核心域,才能发挥它的价值。这种为核心域提供支撑的域即为支撑子域。
- 通用子域 (generic): 通用子域就是整个领域中通用的功能,比如掌管登录鉴权的认证授权域,每个子域都会用到,这种通用的域即为通用子域。
限界上下文 (Bounded Contexts)
限界上下文,是子域之间沟通的桥梁,因为有限界上下文的存在,才使得统一语言变得真正的唯一。限界上下文,博主这里总结五个概念:
- 合伙 (ParnerShip) : 两个子域之间为合伙关系,内部要素共享,无法分开。
- 共享内核 (Shard Kernel): 两个子域之间为紧密合作关系,在代码层面可以抽离出公共的模型作为共享组件。
- 跟随 (Conformist): 两个子域为上下游关系,且一定为绑定关系,但是上游系统为主,自己为从,俗称跟屁虫。
- 客户供应 (Customer Supplier Teams): 两个子域为上下游关系,虽为强依赖,但是讲究平等,共同发展。
- 反腐层(Anticorruption Layer): 两个子域为上下游关系,但是对方都是两个阶级产物,需要一个中间人作为协调。
- 分离(Separate Ways): 各走各的,互不干扰。
我们根据国外一个图来更深入了解下限界上下文的概念:
先对图文中的关键字进行下解释:
OHS/PL
: 开放主机服务/发布语言 其中OHS
可以具体解释为远程调用或消息机制实现 ,PL
可以解释为远程调用或消息机制实现ACL
: 防腐层,一种转换成下游模型概念
Customer Management Context
为用户管理服务,Customer Self-Service Context
用户自服务,在这两个上下文当中,用户管理服务为U
,即上游服务,用户自服务为D
,即为下游服务,这里上游提供服务,下游则被动接受服务,典型的客户供应关系。
Printing Context
打印服务上下文,它是一个上游服务,对外提供打印服务,但是假设它是C语言开发的,它是别人提供的服务,对他人提供的服务,其他子域不信任它,所以需要ACL
来进行反腐处理,转换后才去使用它。
Debt Collection Context
借款服务上下文与Plicy Management Context
贷款管理为共享内核关系,且贷款管理与Risk Management Context
风控管理为合伙关系。贷款的服务则为客户管理的跟随者,因为有客户才能有贷款业务的产生。
战术设计
战略是一个建模,是一个方向,而战术则关注如何具体实现,但是不同点是,战略是DDD的精髓,区别于其他设计模式,而战术设计,不仅可以在DDD项目中发挥作用,在其他非DDD项目,也可以发挥它应有的价值。
战术与战略没有具体的先后,它俩的存在是一个反复的过程,战术根据战略进行设计,相应的战略也可以从战术中获得启发从而改变战略建模,如此,战略跟战术的结合才变得更有意义。
战术设计的要素包含实体(Entity)、值对象 (Value Object)、聚合 (Aggregate)、资源库 (Repository)、工厂 (Factory)以及领域服务 (Domain Service)。
实体 (Entity)
这里的实体不是一个由具体属性去定义的对象,而是一个身份的标识线,他可能横跨了整个系统的生命周期,典型标准就是我们熟知的user_id
,在别的域当中,可能存储了与这条数据关联的用户。他有如下特性:
- 唯一性: 具有唯一的标识,两个即使属性值不一样,但是唯一标识一样的实体,也认为它属于同一实体。
- 可变性: 它具有方法,成员变量,数据可变动
- 生命周期: 存在一套完整的生命周期的管理
实体都有唯一标识,但是具有唯一标示的不一定是实体,重要的是一个对象产生时,就需要为方便系统追踪而创建。
值对象 (Value Object)
简单来说,值对象可以理解为实体所携带的固有信息。比如我有一本《DDD领域驱动设计》书,这里我就是实体,**《DDD领域驱动设计》**就是我的值对象。它有以下特性:
- 不可变性: 他与实体的最大区别就在这,它是一个固有的不可变的对象,一旦创建不可变,比如《DDD领域驱动设计》,你可以扔,可以不看,但是你不能去修改这本书,修改了这本书就不是这本书了
- 可替代性: 虽然它不可变,但是可以完全替换掉,我觉得《DDD领域驱动设计》这本书写的不好,我可以换一本看。
- 整体性: 它不仅可以包含业务数据,也可以包含校验逻辑,以及数据的完整性, 比如一个值对象是6元,看上去是一个实际包含了两个数据,数字6,跟货币单位元。它的用途就是将一些复杂的元素合成一个,变成唯一的一个值对象,可以度量,也可以描述一个事务,度量就是6元,描述,往往指的一个地址,比如中华人民共和国山东省济南市市中区,而且如果你不需要他了,扔掉即可。
在值对象的设计当中,要注意以下几点:
- 首先,值对象跟数据库对象不是一个东西,用惯了现在的ORM数据处理,习惯了把类中类一一映射到数据库当中,但是这种方式在领域驱动设计当中不是不能,而是不推荐
- 其次,在设计
值对象
甚至是实体
的时候,应优先去考虑业务场景,而不是优先去考虑数据库的设计。 - 值对象依托于实体,而实体不建议去依托于值对象。
聚合 (Aggregate)
实体标识一致则为一致的特性,也会有它的副作用,最明显的就是,一个实体的内容修改了,另一个却不同步,为了解决这个问题,所以引入了聚合的概念。它有如下特点:
- 统一管理:在设计过程中,对同一类实体,值对象做统一管理,怎么定义同一类,由自身业务决定,在确保一致性的情况下尽量缩小范围
- 事务一致性:聚合内的实体与值对象,需要有高度的一致性。
- 聚合根: 每一个聚合必定有一个聚合根,所有数据的传递,都有它来决定。
- 聚合根的唯一性: 聚合根必须有唯一标识性,方便其他聚合调用,聚合内部只能对外暴露聚合根,不能直接被其他聚合直接引用,否则将失去应有的意义。
对于上述抽象的可以理解为,你是电商项目组的订单服务组,你跟你的组员都是实体或者值对象,隶属于订单服务组,这个组齐心协力一起去搞定订单服务,一切上级的传达由组长来告知。
工厂 (Factory)
上文中,我们介绍了实体,值对象,聚合,但是你会发现,他们的关系如果创建起来是个极为复杂的过程,而且重复的工作也需要浪费不少的时间,因此,我们需要考虑把创建聚合的过程进行封装,它不会去替代原本的业务,只会把复杂的过程进行封装,这个封装的模式,就为工厂模式,对于工厂模式的介绍与使用,可以参考博主的另一篇博文 设计模式之工厂模式 。
资源库 (Repository)
Repository
这个名词,我们经常见的就是Spring
中的@Repository
注解,代表持久层的意义,那持久层跟这里的资源库的区别在哪呢? 区别就是,资源库专注于对业务的操作,而持久层专注于对表的的CRUD操作,而且它俩并不冲突,在领域层模型中,它是资源库的实现。
在领域模型的资源库,你可以理解为它是一个容器,他主要负责不同聚合之间的管理工作。具体工作如下:
- 分离领域模型与数据模型 他清晰的定义了这两者的边界,由它去对接持久层进行数据的存储,也由他对接客户端,客户端只需对它下达指令,由它去统一调度聚合执行工作。
- 由它去严格的限制只能通过聚合根传递数据
领域服务 (Domain Service)
领域服务,可以理解为在执行一个业务逻辑处理的流程过程当中的服务,它的特点就是:
- 无状态 它是一个一代而过的服务。
- 无法融入 它不属于业务流转过程任何一类,即不属于实体,值对象,聚合等等。
- 内聚转换 它只关注于做一件事情,并且对于业务数据有一定的处理转换操作。
现实中最经典的就是密码加密服务,就是一种无状态的领域服务。
领域建模方法论
微服务项目的拆分是一个非常复杂的过程,往往我们需要考虑拆分后的架构可以演进,迭代,而且还有可能有遗留老系统需要进行兼容,如何跟业务沟通,如何运用DDD来进行领域拆分呢,这里就需要一些方法论的支撑。
什么是方法论
方法论是为用于研究的上下文的框架,它是一种基于观点,信念和价值观的连贯逻辑方案,可指导研究人员或其他用户做出选择。
常见的方法论
- CBM-组件化业务模型
- SOA-面向服务架构模型
- UML-软件设计模型
上面三种都是很好的企业架构的模型,有兴趣的可以自行搜索,它们都跟领域有关,但都不是专有的领域模型。
事件风暴(Event Storming)
DDD的思想很重要,但是也难以掌握,为了掌握DDD所以出现了轻量级的建模方法,就是事件风暴 (Event Storming)。
事件风暴是以探索复杂业务领域为目的的研讨会。目的如下:
- 为统一语言做基础铺垫,使整个商业流程共同理解。
- 事件风暴后,将获得一份整体流程的概览图
- 能在探讨过程当中找出更多的核心价值,机会。
它的优点也很明显:
- 在事件风暴中,不要构建负载的UML关系图,也不用数据建模,有效降低了创建全面业务模型的时间,本来花费几周的事情,可能在一次研讨会中就确立了。
- 统一参与,增加了团队人员的参与感,业务人员与开发人员可以进行良好的沟通,提高了参与者的专业知识。
既然事件风暴的好处这么多,那么如何建立事件风暴呢?
- 找一个可移动的大白板或者一张可收纳的纸,方便进行另一场事件风暴,面积越大越好,因为你无法预测你最后能有多大的产出。
- 准备一堆便签
- 准备开始嘴炮与贴纸游戏,完成事件风暴
事件风暴结束后,你们的成果可能就是这个样子的:
这些五颜六色的纸,在事件风暴的定义中也是有意义的,根据官网大致定义如下:
- 橘色(正方形76*76):Event 事件
- 蓝色(正方形):Command 命令
- 紫色(长方形): Policy/Process 商业政策/流程
- 黄色(小张长方形):Actor 角色
- 黄色(长方形):Aggregate 聚合
- 粉红色(长方形):System 外部系统
- 红色(正方形):Hotspot 热点
- 红色(小张长方形):Problem 疑问
- 绿色(小张长方形):Opportunity 机会
- 绿色(正方形):Read Model 资料读取模型
- 白色(大张正方形):Uset Interface 使用者介面
想更深入了解事件风暴,可以参考官网:
领域模型
领域模型过程当中,我们常听说的有贫血模型、充血模型,那么这里的血是什么呢? 这里的血是业务逻辑。
- 贫血模型: 我们常见的 getter setter方法,天天CRUD
- 充血模型: 应用逻辑为主,数据管理为辅
但是实际当中,它们各有优劣,贫血模型的好处就是上手极快,适用于应用逻辑简单,替代Excel表格的信息记录业务,但是发展很受限制。相应的,充血模型上手极难,但是容易适应复杂的业务场景,可拓展性高。