编辑推荐: |
本文主要介绍了DDD领域驱动设计在微服务架构的应用。希望对你的学习有帮助。
本文来自于CSDN,由火龙果软件Linda编辑、推荐。 |
|
前言
我们都自诩面向对象编程,OOP思想更是熟读于心,然而随着业务日益复杂,代码越来越臃肿,这时感觉之前面向对象的理论也毫无用武之地。到底哪个环节出问题了?笔者认为造成这种局面的原因很大程度是我们忽视了业务建模和设计的重要性。我们通常启动一个项目后,架构师等技术人员会拿到产品人员的产品需求然后开始各种建模、各种拆分,也是在技术内部形成共识和就进入实施阶段。这实际就犯了一个严重的错误:技术和业务未达成业务模型的共识。2003年Eric
Evans发布首版《领域驱动设计》实际就为了解决这个问题。领域驱动设计更大层面是提供了方法论的支持,所以在具体实施各有不同。今天我们就介绍下我们在实践过程领域驱动设计的一些经验与心得。
认知
“Domain-Driven Design领域驱动设计”简称DDD,是一套软件系统分析和设计面向对象建模的方法论。强调通过统一建模语言和理念连接业务人员和技术人员,最终通过领域模型对业务达成共识。
既然要领域驱动设计,那么我们首先就要弄清楚什么是领域模型。简单说我们在业务分析或开发过程中涉及的一切实体如:公司、员工、老师、学生都对应一种领域模型。
领域模型可理解是经过业务建模对客观世界在业务域的一种投影。而领域建模的过程,也是认知多于创造,发现多于发明。领域模型更是开发组织最重要的资产之一,尤其是对于有标准化业务流程的业务公司,譬如:一个航空公司无论技术怎么更新迭代、在航空领域的标准化业务流程一定是不会变的。从技术层面一个领域模型一定是包含领域属性、领域行为的,我们称之为充血模型。而常规开发过程中我们定义的对象往往仅作为数据的载体,亦称之为贫血模型。
价值
通常而言我们解决复杂和大规模软件落地的三把斧:抽象、分治、方法论。在微服务大行其道的今天抽象、分治已经是大型软件架构最基本的法则。在软件架构过程中大家如果完全基于经验进行模块划分、边界定义,这样就加入了太多主观因素也导致不同的人看来就存在诸多认知偏差。这时候就需要引入统一方法论,以方法论为起点统一我们思考问题的基准线。其中DDD提供了一整套完整的方法论,帮助我们更好的理解业务、拆分模块以及制定迭代计划。
困局与转机
既然DDD大家都觉得是一套不错的理论体系,为什么从提出到当日近20年了仍然没有普及和流行起来呢?主要原因在于在DDD的思想下业务分析师必须对业务的现在、将来了如指掌,准确无误的抽象出领域模型。同时系统架构师也必须设计一整套符合DDD开发架构和规范,避免在系统迭代和人员交替的过程中大家追求短平快而打破了DDD价值平衡。DDD的代价就是前置充分的准备,这也恰恰就是它的局限所在。所以在互联网研发体系“小步快跑,快速试错”的大环境下,DDD渐渐沉寂了。
近些年微服务理论被提出、被互联网行业广泛使用,人们似乎又重新发现了领域驱动设计的价值。单一职责、限界上下文无不与DDD思想不谋而合。DDD俨然已经成为微服务拆分的通用法则。微服务的普及也终于证明了DDD的价值所在。
实践出真章
笔者前段时间做过一个教学类业务系统,功能模块包括课程上架、学生报读、考勤管理、在线排课,教学服务等场景,当时我们是以业务中台的思路去规划实现的,也就是会从更高的维度去抽象服务已达到更好的复用性。也正是这种更高的要求,我们从服务拆分、数据库设计到编码我们都充分地实践了领域驱动设计的指导思想,也充分感受了它的科学性。下面分别从上述三个方面阐述我们的实践过程。
服务拆分实践
该项目我们采用主流的Springcloud微服务架构。于是我们践行微服务最高指导思想:单一职责原则。通过前期业务分析我们大致得出下来的业务模型和业务行为。潜移默化的大家可能都会先入为主如此归纳和分析,然后就成为最终模型开始落地编码。
一切看上去也很完美,但是这样拆分确有以下几个问题:
缺乏更高层业务抽象,导致同一业务行为多次实现。譬如:课程排老师、排教室以及查看占用情况实际上属于同一业务行为确需要各自编码。
课程服务框死了课程上架、售卖等行为,无法扩展支持教材单独购买等业务场景。
缺乏基于业务域的抽象,可能出现在业务迭代过程中为了满足一些不确定需求而扭曲之前的设计。
于是我们拿起DDD的武器,分别抽象出两大业务域及两个高层服务。业务域包含:教学资源、教学活动。高层服务域包含:商城服务、排班服务。最终的服务拆分如下:
考虑到拆分服务太多引起的复杂性以及后续一些不确定业务迭代需求。在这里我们其实也做了一个理想与现实的平衡。譬如商城服务,我们暂时就不在细分为订单服务、商品服务等。
服务抽象与数据库设计实践
那么为什么我们要抽象一个排班服务呢?也是得益于领域驱动设计的思想,从现实世界到业务模型共性的抽象,我们可以把排课、排教室甚至后面可能出现的值班等统一抽象为:排班即针对某个资源应用于某个具有时间分布的对象的映射。在这个业务过程中涉及的领域对象就包括:排班资源、排班对象。转换为数据库设计如下:
无论多么巧妙的设计必须服务于业务,否则都是纸上谈兵。贴着业务我们得出推演结果如下:
充血模型编码实践
在DDD理念中强调充血领域模型,在项目中无论是订单、优惠、转班等我们都大量使用到这种编码风格并从中得到便利,下面以优惠券匹配这个业务来整体阐述我们使用DDD编码风格的好处。
优惠匹配逻辑由于涉及各种规则譬如:是否新生可用、满减、多科连报、指定区域或者班级可用、是否可叠加等。而且在计算过程中势必要读取各种数据进行判断。想象一下如果我们按传统的编码方式,代码结果大致如下:
这段代码乍一看也没什么问题,确实也没什么问题。但是在一段复杂代码里面各种分支逻辑夹杂这数据库查找,各种数据获取操作就会让本来足够复杂的代码显得更加复杂,最重要的是我们如果需要测试某个分支逻辑就需要把整个运行环境启动起来并在上下游数据库构造需要的测试数据。想想够头疼吧!那么有什么解决之道呢?那就是DDD领域模型思想。首先我们建立一个优惠计算模型,然后把优惠计算所涉及主体及判断条件都作为这个模型的属性。这样计算模型就包含是否新生、可选优惠券列表等属性方法以及对外暴露的领域行为方法。大致代码结构如下:
那么,这样写到底有哪些优点呢 ?
高内聚低耦合,后续无论外部表结构发生了如何改变都不会影响这个计算模型,你要使用只需要按要求先构建这个模型即可。
同样,要逐个测试几十个分支逻辑不在需要启动整个运行环境了 ,而只需要在一个测试的Main方法搞定。
代码可以做无缝迁移和重用。
一些尝试:事件驱动与CQRS
关注过DDD的一定也听过领域事件和CQRS架构。基于这种理念我们在这个项目也做过一些尝试、虽然不完全按CQRS的架构模式实现,但是理念是应该是大同小异。接下来我们还是先简单了解下一些概念:
领域事件(Domain Events):领域事件是领域驱动设计中的一个概念,用于捕获我们所建模的领域中所发生过的事情。比如用户“登录成功”,学生“完成报读”都是一个领域事件。从业务逻辑来说领域事件关系到整个流程的成功或者失败;同时又将触发后续子流程;而对于业务方来说,领域事件也是业务递进的里程碑。
CQRS:即命令查询的责任分离(Command Query Responsibility Segregation)简称,也是一种广泛应用DDD的一种架构模式。简言之我们提供一个对外接口要么是执行一个Command完成一个动作写入数据,要么执行一个Query查询返回数据。通常我们持久化的数据是需要保证事务性及满足第三范式要求的,而我们查询的数据是为了适配前端显示具有多样性、反范式性的。CQRS正是提供了一套机制帮助我们从数据模型层面实现读写分离。
了解这些概念后那么我们是怎么具体实践的呢?我们以老师登录、查询课程表这个业务动作来讲解。这从后端来说实际就涉及课程表数据怎么存和课程表数据怎么查两个问题?前端交互大致如下:
假设一个班一共有100节课,从业务操作层面只需要设定第一节课日期和周期性规则即可。
如果我们一个班级排课记录就存一条记录,这样对数据写入以及后面的编辑操作是最友好的。但是这样我们无法解决譬如:我们查询某一天需要上课的班级列表等查询需求。最粗暴的方式:数据库按节次存储,有100节课就存100条记录。这样显然不是最优方案。这时我们就采用了事件驱动+CQRS模式,大致方案如下:
数据存储:一个班级对应一个排班规则,即写入一条记录
查询课程表:在redis缓存构建以班级维度、学生维度的课程表热数据
按上课日期查询班级列表:增加一个bitmap字段,0表示当天有课1表示无课程,最主要的是mysql、mongoDB、redis都支持bitmap异或匹配查询。
热点缓存刷新:以领域事件生成MQ消息,譬如完成了排课这个领域事件,下游订阅MQ消息就开始重新构建缓存。
这样,我们就完成了业务层面的读写分离,从而大大降低了耦合。
结语
DDD不是八股文也不是万能药,它只是一种指导我们的方法论。如果你只是生搬硬套显然领域驱动设计或许会让你失望。
DDD强调的是领域模型的建立,虽然Evans大神也提供了一些架构分层样式,但这并非唯一的选项或者适合选项,因为不存在你必须遵循的固定单一的架构样式以实现它。我们在实践过程更多的是要理解DDD思想,然后举一反三,从而更好的服务于我们的业务。 |