最近学习了领域模型,虽然对领域模型中的每个概念都有大致的一个了解,但是总是在脑海里面模棱两可,看了极客 欧老师的DDD模型讲解理论太多,导致自己总是摸不着门路,不知道从何下手,怎么打破平时我们习惯性的E-R图建模思维,故在美团DDD实践的基础上在尝试一下自己模拟一下业务场景从头到尾的搭建一个DDD领域模型的业务场景。美团DDD实战项目链接:https://tech.meituan.com/2017/12/22/ddd-in-practice.html
文章就不从领域模型的概念开始说起了,在下面实际的项目里面会加入自己对于各个领域模块的理解,如果有什么不对的地方希望可以得到大家的纠正。
开场白:
至少30年以前,一些软件设计人员就已经意识到领域建模和设计的重要性,并形成一种思潮,Eric Evans将其定义为领域驱动设计(Domain-Driven Design,简称DDD)。在互联网开发“小步快跑,迭代试错”的大环境下,DDD似乎是一种比较“古老而缓慢”的思想。然而,由于互联网公司也逐渐深入实体经济,业务日益复杂,我们在开发中也越来越多地遇到传统行业软件开发中所面临的问题。在《复杂软件设计之道》里面作者说到了技术负债一词,我在一家千万级别的电商公司上班,听到这个词的时候立马就产生了共鸣,公司前期处于发展阶段,开发人员为了能将产品快速上市,导致各种业务杂乱无章,平时改一个很简单的功能点,改了一个地方,另外一个业务也用到了(老员工也不熟悉业务,代码毫无关联的地方),导致上线总是出现一些这没生效,那没反应的问题。等等
场景需求:
抽奖平台
1.抽奖活动有活动限制,例如用户的抽奖次数限制,抽奖的开始和结束的时间,抽奖人来源的抽奖机会类型(例如分享xx才能抽奖),等;
2.一个抽奖活动包含多个奖品,可以针对一个或多个用户群体,奖品可以指定人群(标签,会员级别)发放;
3.奖品有自身的奖品配置,例如库存量,被抽中的概率等,最多被一个用户抽中的次数等等;
4.用户群体有多种区别方式,如按照用户所在城市区分,按照新老客区分,会员等级,活跃度等;
5.活动具有风控配置,能够限制用户参与抽奖的频率。
大家先是思考下如果按照我们平时的开发习惯肯定就是先把数据结构建好(E-R),然之后开始用curd进行过程式代码的编写。
1、建立活动表(t_activity),活动规则表(a_activity_condition),活动奖品表(t_activity_prize)等
2、生成对应的mapp,dao等
3、建立一个lotteryService,里面存放了大量的mapper,各种if逻辑.
这种业务模型简单的时候还好,等业务复杂起来的话,会出现大量的业务逻辑存放在service里面,业务边界不明确,可能其中的某个方法某个判断也存放在其他的service里面,导致改某个片段的代码,其他的service也影响到了,或者说改ServiceA中的某段代码,ServiceB也影响到了。
平时我们开发维护的时候产品说加一个xx需求,我们每次都需要想这样子改动会不会影响到其他地方,最后的出的结果就是,不确定会不会影响,或者直接和产品说:做不了等。导致产品为了兼顾开发的难度导致调整了需求,本来应该是让技术去适应市场的,现在却倒转过来,导致做出来的东西不符合原本的想法。
DDD领域模型的核心思想就是“高内聚,低耦合”,或者面向对象的设计说的“”,我们平时开发总是将业务逻辑和底层的模块(如mapper,redis的工具代码,es的工具代码)耦合起来,例如上面中的奖品,先出现redis,没有数据就查询mysql,我们一般写成
这方法一般是放在上面的 lotteryService里面,各位想下这本身是一个抽奖应用层,获取奖品本身的逻辑为什么会出现在这里,或者说抽奖逻辑为什么需要知道从redis取还是mysql拿呢?如果改成es?或者是其他的nosql,这样是不是要去修改lotteryService这个类,但是这个本身就不属于抽奖逻辑的,为什么最后需要去修改这个类呢?这就是面向对象说的,封闭原则。职责单一性。
接下来开始使用DDD领域模型去拆解这个抽奖业务
战略设计
战略设计是根据用户旅程分析,找出领域对象和聚合根,对实体和值对象进行聚类组成聚合,划分限界上下文,建立领域模型的过程。战略设计采用的方法是事件风暴,包括:产品愿景、场景分析、领域建模和微服务拆分等几个主要过程。战略设计阶段建议参与人员:领域专家、业务需求方、产品经理、架构师、项目经理、开发经理和测试经理。
是不是很难懂?别急下面我们一步一步来。我们可以把抽奖拆解成C端和M端,M端就是我们常说的管理后台,设计抽奖活动的相关配置。接下来讲解的是C端
根据需求描述我们不难想到核心子域是抽奖子域。我们想下抽奖的逻辑,可以分为1)抽奖前判断用户是否满足抽奖条件;2)获取奖品池中的奖品,根据概率抽奖,得出中奖的奖品 3)得到的奖品进行判断用户是否瞒住得到奖品的资格;4)根据类型发放奖品。
可以得出,这个业务可以分成抽奖前,抽奖后两个领域。
2、战术设计
针对抽奖的子域部分,我们可以得出如下的集合根和实体
UserInfoFacade:防腐层的含义可以理解为适配器,由于UserInfo这个信息是在另外的微服务里面,我们需要通过http去获取相对于的信息,所有需要使用一个适配器去请求,理解为微服务里面我们服务间的接口调用。
同理我们对于发放奖品领域服务可以得出如下的建模
上面就是领域模型的建模过程了,现在把刚刚的业务整理到对应的模块里面,接下来直接上伪代码了。
DDD工程实现
在对上下文进行细化后,我们开始在工程中真正落地DDD。
模块
模块(Module)是DDD中明确提到的一种控制限界上下文的手段,在我们的工程中,一般尽量用一个模块来表示一个领域的限界上下文。
如代码中所示,一般的工程中包的组织方式为{com.公司名.组织架构.业务.上下文.*},这样的组织结构能够明确的将一个上下文限定在包的内部。
代码演示1 抽奖模块
代码演示1:LotteryCondition(活动限制聚合根)
从中可以看到抽奖限制聚合根的主要功能就是整合几个实体或健对象对用户的活动权限进行控制。这一步是纯业务性的,没有涉及到db,缓存,等的操作,因为基础资源性的东西不应该耦合在业务里面。
代码模块2:UserInfoFacde (用户信息防腐层)
这一步可以理解为就是使用适配的方式调用第三方服务(微服务)。因为第三方的服务的参数总是和我们需要使用到的时候是不一样的,我们需要使用适配器的思维调用。
代码模块3:ActivityCondition
重点是checkActivityCondition()方法,做逻辑处理
代码模块4 :LotteryChance
发放奖品聚合根就不上代码了,关于资源库如何引入,我们我们可以看下Prize代码
将资源库的部分从原来的service中抽取出来,和业务分离开来,这是很有必要的,业务本身就不用管你的数据怎么样来,只是负责业务的逻辑。包括现在很流行的六边形架构,整洁架构等都是这个思想。把业务和底层分离,这个底层可以是框架本身,数据库等。
我之前公司由于需要把mysql读取的换成redis或者是本地缓存。如果把mapper代码放到service里面的话,这改动起来是非常巨大的一个工作,还会导致逻辑代码的改动。如果使用DDD的话是不是更加的符合面向对象的开闭原则呢?
最后上一下整个抽奖服务的代码
把具体的逻辑封装到独立的领域里面,这样看起来不是比各种状态散落在service里面舒服的多?服务里面只有领域对象,或者是聚合等。即使要改动数据的存储也不用改动到业务逻辑。