编辑推荐: |
本文主要介绍了DDD设计思想和方法论,同时结合在实际项目中应用总结和思考。希望对您的学习有所帮助。
本文来自于博客园 ,由火龙果软件Linda编辑、推荐。 |
|
领域驱动设计(Domain Driven Design,简称:DDD)设计思想和方法论早在2005年时候就被提出来,但是一直没有被重视和推荐使用,直到2015年之后微服务流行之后,再次被人重视和推荐使用。
下面我来介绍一下DDD设计思想和方法论,同时结合我们在实际项目中应用总结和思考。
目录
1、为什么要用DDD
2、DDD架构设计
3、领域建模方法
4、代码实践
5、问题总结
1、为什么要用DDD
面向过程
很多时候,我们都是操着面向对象的语言干着面向过程的勾当。大部分的系统都是从单一业务开始的。但是随着支持的业务越来越多,代码里面开始出现大量的if-else逻辑,这个时候代码开始有坏味道,没闻到的同学就这么继续往上堆,闻到的同学会重构一下,但因为系统没有统一的可扩展架构,重构的技法也各不相同,这种代码的不一致性也是一种理解上的复杂度。
分层不合理
分层太多和没有分层都会导致系统复杂度的上升。
随心所欲
随心所欲是因为缺少规范和约束。
面向对象
深入的理解面向对象设计原则、模式、方法论有很多,同时要具备非常好的业务理解力和抽象能力。
SOLID是单一职责原则(SRP),开闭原则(OCP),里氏替换原则(LSP),接口隔离原则(ISP)和依赖倒置原则(DIP)的缩写,原则是要比模式更基础更重要的指导准则,深入理解面向对象设计原则极大的提升我们的面向对象设计的能力和代码质量。
分层设计
分层最大的好处就是分离关注点,让每一层只解决该层关注的问题,从而将复杂的问题简化,起到分而治之的作用。
领域建模
领域模型可以准确地反映业务语言,使业务语义显性化,而传统的J2EE或Spring+Hibernate/Mybatis等事务性编程模型只关心数据,这些数据对象除了简单setter/getter方法外,没有任何业务方法,被称为成贫血模式。
规范设计
各归其位、命名约定、通用业务状态码约定等。
DDD概念定义
领域驱动设计:一种软件开发方法,是一种软件核心复杂性应对之道,它可以帮助我们设计高质量的软件模型。
DDD目的:
DDD是为了解决复杂业务逻辑的问题
DDD的核心问题不是技术问题,而是关于讨论、聆听、理解、发现业务价值的问题
DDD是技术人员拥有产品思维的一个体现
DDD的学习曲线很陡主要是因为它是一种方法论,每个人对它的理解存在差异
领域模型与事务脚本对比
富血模型:就是属性和方法都封装在Domain Object里,所有业务都直接操作Domain Object。
service层只是完成一些简单的事务之类的,甚至可以不用service层。也就是直接从action->entity。
贫血模型:就是一个对象里只有属性,要实现业务,要依靠service层来实现相关方法,service层的实现是面向过程的,大量传统的分层应用action->service->dao->entity的方式就是这种贫血的模式实现。
举个例子,银行转账实现类
各位看官看到上面的代码是不是有一种似曾相似的感觉?咋一看也没有啥问题,能正常运行,能快速交付业务。如果仅是为了应付需求任务交差,当然没有什么啥问题了。
从设计角度来看确实存在一些问题,代码糊在一起,没有分层设计,伪面向对象的方法。
我们码农总得要有自己一点的追求,为伟大代码事业创造一点艺术贡献!
如果业务需求快速变化怎么办,需求越来越复杂怎么办?
如果团队里面有多人协作,代码需要经过多人反复修改接手传承,你敢保证后面接手的人不会骂你吗?
难道设计上有没有可以改进的空间?
答案是正面的!
客观来说上面的这段代码不算复杂,也算写得中规中举。下面我来让贴一段曾经在我们实际生产系统跑了将近一年的神代码,你才知道什么叫复杂和神奇!16层if..else+for循环!就问你怕了没有?
》》》请点开看下面神码片段����
请看神码--地狱18层!
各位看官你们看,看完是不是很想吐血!像上面这种代码请问谁敢去维护?谁看谁骂,谁改谁死!这种神代码绝对是可以拿来当教材用的~(在这里展示这段代码主要为了说明如果系统设计和分层不合理,将会带来严重的后果,可以说代码就像狗屎一样)
PS:说明一下当时写这些代码的作者因为一些原因离职了,我们当年系统上线后将近一年多的时间里不敢去修改这神代码,也没有人敢改。
如果业务需求一直稳定不改,那可以还勉强维持着,但是那有正常业务不改需求的?天底下应该没有!所以它始终像一颗大雷在我们的头顶上滚动着!
后来经过两次重大重构设计之后,把它消灭了!篇幅原因这里的故事就不展开讲了,有兴趣的请看另一篇文章:《那些年那些神码》。
回到主题上,银行转账那一段代码对比之下是不是小屋见大屋?下面我们基于银行转账那一段代码,展示一下怎么拆解和设计,变成领域设计模型。
转变成领域设计如下:
抽出Account Domain类;
抽出转账类即领域服务;
抽出透支策略接口与实现类
重新拆解转换之后,设计领域对象,代码分层结构清晰了很多,后续业务折腾变化容易维护和扩展,从此世界也变得清新了,你说香不香吗?
2、领域驱动设计架构设计
2.1、分层结构转变
先看分层结构思路转变,领域驱动设计分层与传统三层结构有比较直观的区别。
User Interface为用户接口层,也是经常我们看到Controller层类似,主要负责系统入口协议等,该层不处理任何业务逻辑,只负责调用接入和应用转发。
Application是应用层,和以往事务脚本的Service是截然不同的,该层中并不做详细的业务逻辑的封装,而是创建Domian并调用Domain中的相应方法完成业务逻辑处理,并调用Infrastructure完成Domain的持久化操作,该层需要负责事务的控制,保证数据的一致性。
Domain层是核心领域层,核心的业务逻辑应该以Domain为对象进行分类封装,Domain的划分以业务为基准,Domain层在技术层面的建模通用技巧在下面会做详细介绍,该层只能向下调用Infrastructure完成自身模型的初始化和持久化。
Infrastructure层类似于以往事务脚本的Dao层,以往的面向事务脚本的编程中,以数据表为主,所有的事务脚本直指目的就是完成表的CURD,而DDD中以模型为核心,Infrastructure层是为了重建已有的Domain,并在退出时持久化内存中的Domain对象。Infrastructure层不仅包含对关系数据库的处理,还包括对分布式缓存处理、对外系统的接入(integration)以及分布式消息队列的push操作。
一些常用的关键术语:
实体 - Entity
值对象 - Value Objects
领域服务 - Domain Services
领域事件 - Domain Events
模块 - Modules
聚合 - Aggregate
资源库 - Repository
实体和值对象在代码中皆表示为一个类(对象),从业务上来讲也分别表示一个业务实体。但是两者是有区别,实体是一个完整的具有生命周期的可以通过唯一标识来识别东西的类(对象),而值对象则为了度量和描述领域中的一个属性,将不同的相关属性组合成一个概念整体的类(对象)
。
例如,客户可以认为是一个实体,一个客户就是具有生命周期的东西,具有唯一的标识可以将A客户和B客户分开(唯一标识:身份证号码),而这个客户的地址(例如:广州市/白云区/欧派软件园)就应该定义为一个值对象,当我们定义好一个地址,它既可以是A客户的地址也可以是B客户的地址,当我需要更新A客户地址时,可以直接销毁A客户关联的地址对象然后重新创建一个地址对象关联到A客户上。
领域事件是由一个特定领域因为一个用户Command触发的发生在过去的行为产生的事件,而这个事件是系统中其它部分感兴趣的。
在现在的分布式环境下,业务系统与业务系统之间并不是割裂的,而消息绝对是系统之间耦合度最低,最健壮,最容易扩展的一种通信机制。
领域服务和以往事务脚本的Service只能说非常像,唯一不同的是,以往事务脚本的Service是自己全权负责业务逻辑,而领域服务不仅编写业务逻辑,还会调用实体和值对象的方法来协调实体和值对象,从而避免实体和值对象的耦合。当我们建模之后发现有些业务既不单单属于A领域对象,又不单单属于B领域对象,我们可以那么我们可以抽象出一个Service来完成此项业务,那么这个类就是领域服务。
资源库也叫数据仓库,主要是完成领域对象的重建以及对象的持久化操作。资源库的设计主要是为了调用基础设施层来完成表的CURD、缓存的操作以及外系统接口的调用。
2.2、领域驱动设计
这里引用阿里团队开源的可乐(Cola)领域设计架构示图,如下图所示:
DDD最核心思想是设计内部六边形结构。
从内往外看,最内层也是最核心就是Domain层(包括:Domain model和Domain Service);
第二层是Application层(包括:Application Service);
第三层业务逻辑层(Business Logic也可以叫基础设施层)主对外提供服务接口,对内解决基础服务包装构建。
2.3、分层调用示图
DDD分层架构设计之调用示图
上图已经非常清楚展示了分层设计以及各层调用关系,一图胜千言,大家认真详细看图就可以理解了。
2.4、命令式读写分离
操作命令和对象抽象,通过命令与查询分离设计方式实现服务接口调用。
CQRS(Command Query Separation,命令查询分离) 基本思想在于,任何一个对象的方法可以分为两大类:
命令(Command):不返回任何结果(void),但会改变对象的状态
查询(Query):返回结果,但是不会改变对象的状态,对系统没有副作用
2.5、扩展点
领域驱动设计另外一个重要的地方是扩展点。
可乐包在扩展点功能设计中引入的概念:业务、用例、场景。
业务(Business):就是一个自负盈亏的财务主体,比如tmall、淘宝和零售通就是三个不同的业务。
用例(Use Case):描述了用户和系统之间的互动,每个用例提供了一个或多个场景。比如,支付订单就是一个典型的用例。
场景(Scenario):场景也被称为用例的实例,包括用例所有的可能情况(正常的和异常的)。比如对于“订单支付”这个用例,就有“可以使用花呗”,“支付宝余额不足”,“银行账户余额不足”等多个场景。
例如我们要实现右图中所展示的扩展:在tmall这个业务下——下单用例——88VIP场景——用户身份校验进行扩展,我们只需要声明一个如下的扩展实现(Extension)就可以了。
3、领域建模
领域架构设计并不复杂,复杂的业务需求怎么转化为领域模型,这也是最难的地方,需要业务梳理足够清晰,同时需要有足够抽象能力和领域划分。
领域建模基于业务的建模的基础上,需要花较重的时间比例在梳理业务和模型设计上面;而同时领域建没有通用的统一结构设计,得看具体业务具体分析。下面举个例子说明一下领域建模方法。希望能够各位得到一点启发。
领域模型不是简单POJO或数据实体对象,他还有行为和状态,主要体现在事件机制、值对象上面。
这里先不深入讨论,先抛个影子,后面抽空补上更详细的说明。
4、领域驱动设计实现
4.1、分层设计
各层分工:
Application层主要负责获取输入,组装context,做输入校验,发送消息给领域层做业务处理,监听确认消息;
Domain层主要是通过领域服务(Domain Service),领域对象(Domain Object)的交互,对上层提供业务逻辑的处理,然后调用下层Repository做持久化处理;
Infrastructure层主要包含Repository,Config,Common和message,Repository负责数据的CRUD操作,数据来源可以是MySQL,NoSql,Search,甚至是HSF等;
Config负责应用的配置;Common是一写工具类;负责message通信的也应该放在这一层。
4.2、建立二方库组件
二方库组件:
api:存放的是应用对外的接口。
dto.domainmodel:用来做数据传输的轻量级领域对象。
dto.domainevent: 用来做数据传输的领域事件。
Application里的组件:
service:接口实现的facade,没有业务逻辑,可以包含对不同终端的adapter。
eventhandler:处理领域事件,包括本域的和外域的。
executor:用来处理(Command)和查询(Query)。
interceptor:COLA提供的对所有请求的AOP处理机制。
Domain里的组件:
domain:领域实体,允许继承domainmodel。
domainservice: 领域服务,用来提供更粗粒度的领域能力。
gateway:对外依赖的网关接口,包括存储、RPC、Search等。
大部分情况下,二方库的确是用来定义服务接口和数据协议的。但是二方库区别于JSON的地方是它不仅仅是协议,它还是一个Java对象,一个Jar包。既然是Java对象,就意味着我们就有可能让DTO升级为一个Domain
Model(有数据,有行为,有继承) 。
Domain Model用到的所有数据如果都是自恰的,那么这些计算是不需要借助外面的辅助,自己就能完成。比如CustomerCO拥有身份证号码,那么判断当前这个CustomerCO的性别、年龄等信息时是依靠自己(身份证号码)就能完成的。是一种共享内核,
CustomerCO把自己领域的知识(语言、数据和行为)通过二方库暴露出去了,假如有100个应用需要获取性别或年龄时做判断,客户端就不需要自己实现了。
4.3、上下文
应用不同Bounded Context之间的协作,要充分利用好二方库的桥梁作用。其协作方式如下图所示:
4.4、主要组件依赖关系
依赖关系示例,如以Customer会员业务对象举例如下图所示
4.5、代码实现
下面以我们系统中客户中心会员体系设计为示例介绍一下怎么使用DDD方法实现代码。
对外接口代码示例
参数校验
API接口服务层示例
命令总线示例
全局异常捕获示例代码
4.6、旧项目神码改造
对于旧项目代码,大家都非常头痛,旧系统经常出现一些奇怪的问题,其实就是不稳定引起的。旧系统代码都是神码具多,极难维护,谁都不愿意接手,后来经过我们重大讨论决策后决定对旧项目进行重构。
重构说起来简单,实施起来却是非常的头大,毕竟不是简单的系统,同时也是公司里面业务最重要的业务系统(订单+CRM集团业绩的入口保障),不容许出错;而旧代码库非常庞大,规模接近百万行。代码质量不堪回首,都是地狱级别。
我们痛定思痛,决定对它动用外科手术,当时顶着巨大压力说服大boss同意,游说业务、产品、测试、开发各方一起协作。在保证业务规则和逻辑前提,进行重大的重构设计,主要也是采用DDD领域驱动设计。
这次重构经历了近6个月,顶着各方巨大的压力。但经过几次重大升级发布,终于彻底改头换面,神码级旧系统完成改造。
1个java类近万行神码经过重构改造,确切的说应该重设计重新写代码结构,总结一下有几条宝贵经验:
根据DDD读写分离设计,写入通过实现不同的Command执行器,查询实现不同的查询执行器。
根据不同业务场景增加多个不同的扩展点,有效地解耦业务。
复杂的业务规则引入规则引擎,把业务规则抽象成一条条可动态编辑和可维护规则,并实现动态加载和配置,而不是硬代码。
经过一顿猛烈改造设计,新版本的代码清爽多了!
把一万行的代码直接搞成了360行左右!
旧代码:
消灭了18层地狱式代码
改造后新代码结构:
查询统一执行器如下图所示:
扩展点抽象查询器
扩展点执行器抽象类
经过一番猛操作改造之后,代码简洁很多,变得可读可维护,从此世界清新很多,维护代价极大的变小!
5、问题与总结
软件的世界里没有银弹!因此领域模型还是事务脚本没有对错之分, 关键看是否合适:
对于简单的业务场景,使用事务脚本,其优点是简单、直观、易上手;
对于复杂的业务场景,比较有效的治理办法就是领域建模,在封装业务逻辑的同时,提升了对象的内聚性和重用性,因为使用了通用语言,使得隐藏的业务逻辑得到显性化表达,使得复杂性治理成为可能。
领域驱动设计结构非常清晰,适合复杂业务场景,代码结构清晰,代码维护代会低很多。当然也需要业务建模与抽象能力,与传统面象数据开发方法有本质的不同,需要转变开发者的思维方法,对团队有一定学习成本。
要求开发者:
有开发卓越软件的激情和毅力
渴望学习和进步
有能力理解软件模式,并懂得如何应用这些模式
有发掘不同的设计方法能力和耐性
勇于改变现状
希望编写出更好的代码 |