第三篇:工具篇之DDD(二) 领域驱动实战

一、前言

上一篇文章 领域驱动之领域模型,简单的学一学 DDD 了解了一些 DDD 理论,然而 DDD 难点和精髓却是在其实战的落地上,如今的社会不缺乏理论依据和理论指导,但是实实在在的落地似乎成了永远无法落地的承诺。

话不多说今天我们拿打车软件作为我们实战的载体,例如滴滴打车、美团打车、T3出行等等。

全文思路先从功能分析,到 DDD 的战略设,再到 DDD 的战术设计,最后 基于现实业务的 DDD 实战落地。

二、开始战斗

2.1 功能分析

滴滴打车、美团打车、T3出行这些打车软件都有三个大家熟知且经常使用的功能:

  • 预约用车(one by one)

  • 代人叫车

  • 拼车

其实作为一个平台,可能还会提供一些例如租车服务、找代价服务、规格较高的接待服务等,当然这些功能大家在美团和滴滴这种 2C 的软件中很少见到,但是对于用车领域或者将来三分天下归一大统,2C 和 2B 的业务集于一身,反过来看我们今天的实战,就会变得有意义。实际上租车、代价、接待这些功能只是没有在美团和滴滴中存活下来,实际应用中真是存在。 总结下来用车领域可提供的功能包括:

  • 日常的预约用车

  • 日常的拼车

  • 日常代人叫车

  • 租车服务

  • 代驾服务

  • 短期承包接待服务

  • ......

从功能层面我们可以很容易的去理解以上罗列的逻辑甚至可以简单的猜测出后台是如何实现的,同时也难免会发现一个问题,很多功能实际上是比较类似的,例如日常的预约用车和短期承包接待服务,其核心逻辑还是预约车辆,仿佛日常的预约用车是短期承包接待服务的一个子集。而日常拼车其实也是预约用车的意思,只是多了一个拼车的概念,于是这些功能字面上意思不通,但实际上又比较雷同。

软件开发比较有意思的事情就在此时此刻发生。在软件的虚拟世界中,如何将这些完美的呈现,同时也是对现实世界实体的完美映射,这些都是我们程序员穷其一生需要追求和完成的事情。

题外话:中国男足主教练李铁,在世预赛第五轮1-1被阿曼逼平后的第二天发了个朋友圈,说在比赛结束后早早的就睡了,说自己一直认真工作且无愧于心,做了自己该做的,自己努力认真工作就够了,自己所做的对的其自己的工作。

网上很多人炮轰“早早就睡了”这句看起来被逼平没有带队总结反思而不负责任的话,但我其实在意的是后面他说认真工作就够了。记得几年李铁想竞争男足主教练,说外国教练看不起国产教练,李贴说了句:国产教练怎么了,我们也认真工作努力执教。

我想说的是这就是意识上的差距,当国家队主教练,这不是一份工作,而是事业!光荣而伟大的事业!再说不是认真工作就可以把事情做好,国产教练光有认真工作是不够的,竞技体育需要认真,更需要天赋、意识、体育情商。抹去了中国队出现的可能性,或者说出线可能今生0.04%后,只是认为自己认真对的起这份工作,天大的笑话 。

同样的,作为我们软件开发的从业者,很多人而且绝大对数人把他当作工作,白天上班认真工作,然而这就跟李铁执教男足国家队一样,意识严重不足,所以屏幕前的您,是在完成工作还是在从事一项事业呢?

2.2 领域模型分析

首先我们要分析的是业务流程,通程我们采用流程图来告诉大家,我们当前的业务什么样的状况,有哪些核心逻辑有哪些支撑业务,这也是我们领域分析的起点。

其次我们就要开始区分我们的核心、支撑、通用业务域,其次是子域,在领域驱动中还包括了限界上下文、对象、领域类型等,七其中对象又包括了聚合根、实体、值对象。

最后我们通过时序图、用例图等多种方式呈现业务流程,同样的有多种呈现形式,例如业务用例图、系统用例图、微服务间的系统链路时序图、功能的逻辑时序图等等。

2.2.1 业务分析-从业务流程图开始

流程图应该是从比较粗粒度到详细流程逐步分解的一个过程,此处我们将司机和乘客的核心业务流程呈现出来,其实我们所有的领域划分都是从粗粒度的业务流程图和细颗粒的业务流程图抽象分析得到。

1. 司机核心业务流程

2. 乘客核心业务流程

以上两个流程图只是用车领域的其中两个比较核心的业务流程,我们需要根据功能设计逐一的将所有的业务流程罗列画出来,这样才能保证我们从业务流程中分析出来的领域模型相对的完善。

其实从业务流程到领域的划分,这中间其实是有一个思维的转变、分析、总结的过程,在功能分析小节中提到过,光是认真努力是不够的,用再多的时间将业务流程罗列画出来,无法从业务流程抽丝剥茧分析出领域模型也是毫无意义的!

所以这也再次印证李铁的所谓认真工作对对得起国足主教练这份工作,这种想法是多么的愚蠢而自私,甚至是一种无能的表现。

2.2.2 业务域、限界上下文、子域划分

业务域、限界上下文、子域划分的底层逻辑均是来自于业务流程图,业务流程是业务方提供,通过业务和技术的转化,行程相对规范和标准的业务流程图。从大的业务流程到较小颗粒度的业务流程,实际上流程图的节点颗粒度越小,对于从业务流程转化领域划分的难度就越小。

其次业务域的划分也跟经验有关系,所以很多书籍上说需要领域专家来帮助甚至主导领域建模这项工作。但是实际上在我们日常的产品建设和项目推进过程,基本都是直接参与软件的开发者进行领域建模工作。经验主义在领域建模过程中个人认为还是一个比较好的解决问题的途径,不是所有问题都有公式解决方案,当然经验主义也无法解决一切问题,物极必反,否极泰来。

基于我们领域模型的分析思路,整个用车领域的主要业务域可分为:

  • 用车订单域(核心域)

    • 限界上下文:车辆预约【子域:预约用车、日常的拼车、日常代人叫车、租车服务、代驾服务、短期承包接待服务】

    • 限界上下文:订单管理【子域:订单管理、订单预警、订单评价】

  • 车辆管理域(核心)

    • 限界上下文:车辆管理【子域:车辆采购、车辆租赁、车辆维修等】

  • 费用域(支撑域)

    • 限界上下文:费用管理【子域:燃油费、维修费等】

  • 结算域(支撑域)

    • 限界上下文:核算【子域:个人费用结算、组织费用结算】

    • 限界上下文:发票【子域:发票】

  • 支付域(支撑域)

    • 限界上下文:支付【子域:个人支付、组织支付】

  • 评价域(支撑域)

    • 限界上下文:评价【子域:服务评价】

  • 用户域(通用域)

    • 限界上下文:客户【子域:C 端客户】

    • 限界上下文:用户【子域:司机】

    • 限界行下文:管理员【子域:运营、维护人员】

  • 消息域(通用域)

    • 限界上下文:消息通知【子域:消息】

2.2.3 基于领域模型的业务分析-时序图

从网上找了几个车辆服务相关的业务时序图,实际上时序图就是在领域模型划分好之后,是我们对于业务更深一层此了解的呈现。

1.参与者和对象构成的时序图

客户预订车辆的时序图

客户取车时序图

客户还车时序图

2. 基于领域模型设计的业务时序图

以上三个时序图其实都是以对象作为核心,即参与系统的参与者和对象有哪些,其实我们领域模型设计的比较重要的目的之一,就是在我们当下微服务的架构下,我们应该如何服务划分和服务调用。那显而易见的整个时序图的核心不在是系统的参与者和对象,而是在一个完整的业务流程中,服务间的调用(或者说接口之间的调用)是如何进行的。以往在 MVC 的模式下就是各种接口之间的调用,即 Intface 的 service 的 class 之间的调用,现在我们需要弄清楚的是 Micro Service 之间的调用。

通常情况下我们通过对领域分析的设计,会和应用架构图相互呼应,一个独立的领域大概率会被定义成为一个独立的应用,独立部署独立运行这也就同时带出了微服务架构下各种数据一致性、分布式事务等问题,这不在我们的讨论范畴。所以我们可以简单的理解为领域模型设计带来的价值之一就是让我们可以明白我们的微服务到底该如何划分以及如何相互之间进行调用,其整个的链路是怎样的。

下图是短期承包接待服务的时序图,这种时序图就可以明显的告诉我们在该场景下,各个微服务时间的调用情况以及调用链路。

2.3 实战落地

从现在开始我们进行代码层面的实战落地,我们说领域模型分为战略设计和战术设计,上面的内容可以认为是战略设计,那战术设计基本算就是比较微观的,聚焦到代码层面的落地相关事宜。

实际上 DDD 的代码落地模式和思路有很多,没有说非得拘泥于一种形式,我们常见的 DDD 代码落地一般会把应用分为四个模块:

  • interface

  • application

  • domain

  • infrastructure

无论是新应用的建设还是老应用基于 DDD 进行改造,可预见性的是没有公司可以完全按照此目录进行改造或者建设,试想一下无论是阿里、京东还是美团和拼多多腾讯,每家都有各自的代码规范、统一开发平台、统一部署平台甚至有些大公司提供了基于集团统一技术的代码生成工具,通过此工具生成的项目核心目录是固定的,不能随意更改的,那怎么办?

归根究底 DDD 的落地和改造工作是一个灵活多变融会贯通的事情,这也就要求想要搞定 DDD ,没有一些技术沉淀积累或者技术经验丰富的软件开发人员,落地和实施确实会遇到比较大的阻力,阻力的体现一个是因为经验的不足导致的难度倍增难以坚持,另一方面是认值不足导致的半途而废。

2.3.1 原生 DDD 代码落地分析

刚才我们有提到,原生或者说官方 DDD 将我们的应用分成了 interface、application、domain、infrastructure,而每一个模块又可以进行细分,例如:

1. interface 内部划分

M1、M2、M3表示子模块,每个模块都有三个核心包,即 web、facade、dto,web 的核心子包有 controller、vo,facade 的核心子包有 intf、impl。

实际上从图中可大概能看到,同我们以前的 MVC 模式比,如果加上 DAO 和 MAPPER,单单是 interface 就可以是一个逻辑闭环的应用结构了。

而我们这里的 web 大家耳熟能详,controlelr 就是我们编写 Restful 接口的地方,VO 是可以和前端功能页面可直接 one to one 的模块,其他的不用再过多解释,那 facade 是什么?我们认为它叫防腐层,不仅仅拥有自己的接口还具备了接口实现的能力。既然是防腐层,必不可免的这里面会存在很多定制甚至写死的代码,那这是一种好的设计吗?

答案肯定是仁者见仁智者见智,但是我们站在领域模型的建设思路上,领域是我们的核心,是我们要重点保护的对象,所谓鱼与熊掌不可兼得,总要有牺牲成全别人,所以此处我们姑且认为是可取可行的。

2. application 内部划分

application 被定义为应用层,内容比较简单就是接口定义和接口的实现,那这里有个疑问,为什么没有跟 interface 一样按照功能进行拆包呢?

其实如果每个模块都拆包,无疑增加了代码结构的负责都和冗余性,但这并不是最主要的,我们还是应该从正确的思路和逻辑去思考这件事情。之前我们按照业务流程将领域模型进行了划分,有订单服务、评价服务、车辆服务、结算服务等等,那领域模型划分的价值和目的之一就是给我们拆分微服务提供理论和事实依据。

所以在 application 应用层,其从下层对上层提供的应用服务必然也要遵循某种逻辑,接口的定义也是符合这种逻辑,而不能随意定义,所以我们接可以理性的下一个结论,应用层的接口定义其实也是需要按照我们领域模型的划分逻辑进行定义。

也就是在 application 中我们有 IOrderSevie、IEvaluateService、ISettlementService、IVehicleService 等。综上所述 application 的 intf 在一个微服务的应用中并不会泛滥成灾。

如果在这一层中您的应用里接口定义非常多有可能说明您整个微服务应用的拆机颗粒度较大,也有可能是开发人员在 application 层中没有遵循开发规范定义过于随意。

3. domain 内部划分

domain 顾名思义领域的意思, domain 也是我们整个领域模型设计中最核心、最重要的部分,我们所 有的战略设计和战术设计的最终目的都是为了使我们 domain 的内容相对稳定,无论业务如何变化,我 们的核心技术能力可以始终得到服用,另外有一个问题大家想一想,domain 是否稳定有一个非常重要 的先决条件:数据库表结构不变,如果数据库表结构变了,domain 一样可能会存在极其不稳定的情 况,具体表现就是各种改实体类、调用 MAPPER 接口参数的相关代码,这个问题如何解?我们最后在解 答。 domain 层我们要按照模块进行划分,因为其内容非常的多,从图中可以看到有 module/entity ,即模 型或者叫实体类,而且这里的模型从领域模型设计的角度是分为了实体和值对象,同时实体是一种充血 模型而非贫血模型。


贫血模型 贫血模型:是指领域对象里只有get和set方法,或者包含少量的CRUD方法,所有的业务逻辑都不包含在 内而是放在Business Logic层。


充血模型 充血模型: 层次结构和上面的差不多,不过大多业务逻辑和持久化放在Domain Object里面,Business Logic只是简单封装部分业务逻辑以及控制事务、权限等,这样层次结构就变成Client->(Business Facade)->Business Logic->Domain Object->Data Access。


采用充血模型,我们的实体或者值对象的定义无论从工作流还是从逻辑复杂度将急剧上升,不再是过去 简简单单的属性和 get/set 方法的定义。 exception 很好理解,异常定义,但是这么多年无论是刚毕业的学生,还是已经工作了5、6年的相对有 工作经验的开发者,其实对于异常的定义和使用一直是稀里糊涂,此处不再过多阐述,未来有机会我们 可以另起一篇来探讨 。

4. infrastructure 内部划分

 

infrastructure 内部按照功能模块进行划分,每个功能模块中又分为 entity、mapper、converter、respository.impl。

entity 和 mapper 比较好理解,跟我们认知的 MVC 模式的 DAO 层基本一致,converter 也比较好理解,用于转换,包括不限于对象转化。

infrastructure 比较让我们产生迷糊的是 respository.impl ,它实际上通过注入 mapper 来实现方式数据持久化层的,仿佛没有什么作用,不过经过细想它的存在还是有必要的。很多公司在 web 层规定对象要定义成 VO ,在 service 层规定对象定义长 BO ,在 dao 层对象定义成 PO 或者 DO,这必不可免的就是各种类型的转换,问题是在哪里转换?

以往我们在 conteroller 转换,在 service 转换,于是我们就会看到在 controller 有各种转换的代码,在 service 有各种转换的代码,VO BO DO PO DTO 手误转错的也不在少数,如果有了 respository.impil 的存在,起码我们可以把这些工作放到这个里面。

另外我们如果想要提供原型性的一些领域驱动能力,通过 respository.impl 也可以比较清晰和轻易的实现,否则我们还是只能在 service 中引用各种 mapper,然后为了数据一致性增加事务注解,不是不可以,但是还有更好的是实现方式。

此时此刻隐约间似乎有一个小小的结论,如果您的项目不够复杂体量不大,强行使用 DDD 然后定义各种的 respository 难免会得到一个结论: 其实 DDD 可有可无。

三、结束

到这 DDD 的战术分析以及具体的代码层级解释基本结束,本篇文章基本告一段落,不知道各位读者对 DDD 有没有一点点的感悟或者开窍,下篇我们将继续解密 DDD 在实战中应该如何落地。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值