前言
本篇是在领域驱动设计理论上加以思考后提出的一些观点,适用于对DDD 有一定了解的人员阅读,另外本篇偏向于实践而不是论述理论。请悉知~
1. Domain 层的模型驱动开发
在DDD 的项目上,当我们经历过了事件风暴->命令风暴->划分限界上下文后。我们就会产出一份基本上可以映射至代码实现的领域模型了。这份模型图上已经包含了entity、value object、聚合根等类。且对应的类图上已经有了属性以及实现的方法。我们可以基于这种领域模型很轻松的实现我们的代码。这就是Domain 层的模型驱动开发,我们在实现Domain层的逻辑时,确实是完完整整的参照领域模型里的图来实现代码的。领域模型样例如下图:
当我们有了领域模型后,我们会发现实现Domain的代码是如此简单。它其实是将我们代码设计过程提前了,且可视化出来了。这样能够让大家都有一致的理解,也能够更早的发现问题。
不过值得一提的是,有时候在根据领域模型实现代码时,我们会发现之前在建模时没有考虑到的地方亦或设计不合理的地方。这个时候我们并不会继续照搬模型,我们应该对模型进行重新建模,在没有问题之后再继续实现我们的代码。它就像测试驱动开发一样,是一个双向绑定的过程,我们既能够通过模型分析出设计不合理的地方,也能够在实现模型的过程中发现设计不合理的地方并及时更新模型。它是一个双向验证的关系。
在Domain层,我们已经可以通过之前的领域建模实现出领域层的代码了,但是这些模型并没有指示我们应该如何实现应用层的代码,比如:我们的接口应如何暴露、数据如何存储、和其它系统或服务之间的交互应如何实现呢?我们仅有领域建模来驱动是不够的,所以我们还需要另外一层的模型来驱动开发。
2. Application 层的模型驱动开发
既然我们要使用Application 层的模型驱动开发,那么我们首先要有模型可依,所以我们要先对它进行建模。我们依然使用工具画出我们期望实现的交互方式,这一步的建模就不再需要一个团队来讨论完成了,它是由当前开发某一功能的开发人员来绘制的。这个模型图上描绘了要暴露的功能是什么,要用到的Domain功能有哪些,还会与哪些服务或外部系统来交互等。
当我们完成了Application 层的建模,那么我们就可以按照我们建好的模型进行开发实现了。同样,模型与代码也是一个双向绑定的过程。我们在实现代码时,同样会进行更细致的考虑,当我们发现设计的模型存在缺陷时,我们应重新调整模型,然后再继续开发实现。这样就形成了模型指导开发,开发验证模型是否完备,最终我们会产出一份能够完全映射代码实现的模型与实现的代码。
2.1 Application 层的建模工具推荐
我在实践时一直使用的Plant UML来进行建模,它可以让我们向开发代码一样来编写我们的模型。
强烈推荐使用如下的框架来构建模型,因为它简单易用且模型精美
// import 链接,直接放在文件最上层就可以使用
!includeurl https://raw.githubusercontent.com/RicardoNiepel/C4-PlantUML/master/C4_Component.puml
2.2 Application 层的建模并不会影响开发效率
我们要认识到一点,我们在对Application进行建模时,并不会影响我们的开发效率,相反,它反而会帮我们理顺思路,减少代码返工以及bug的产生。
因为我们在建模的过程,其实就是开发人员在实现代码前拆分task的过程。有一个可能大家有过的经历,在做一个复杂的功能时,没有提前拆分task,在实现的过程中经常遗漏各种要实现的细节。而现在我们在建模时,就会考虑这些实现细节,待我们实现时,就按照模型上的描述一步一步实现就可以。
2.3 将Application 层的建模保存至代码实现的同一目录下
我们可以将开发阶段建立的Application模型与代码一同存放至Application层,推荐与Controller 放在一起,一个Controller与其对应的多个模型图放在同一个包下。
那么我们为什么要存放这些模型图呢?你们可能会想,如果我们想了解代码怎么实现的话,直接看代码就可以了,何必要多此一举呢?其实不是这样的,下面我们来看一个我真实经历过的例子。
这是一个有关系统的登录功能的开发,我先以流水线的方式叙述一下它的登录功能。
- 用户访问登录页面
- 前端首先需要掉后端的某A接口获取加密后的第三方单点登录链接来进行前端页面的跳转
- 用户在第三方登录完成后,前端会从第三方页面拿到一个code,再次调用后端某B接口来解析Code拿到用户的username在后端校验后并暂时缓存
- code解析成功后,用户需要进行短信验证码验证,这时前端又会调用后端某接口C来发送验证码(单拎出来发送验证码的接口是为了重复发送验证码)
- 用户输入完验证码后,前端再调用后端某D接口来验证是否正确,至此登录完成。
可以看到,在登录这一流程里,前端总共向后端调用了4次接口,而且在这个过程中前后端的交互是紧密联系在一起的。这4个接口在后端确实放在同一个Controller下,但是我们单看这四个接口的代码实现,并不能清楚的理解到登录的实现流程。况且前后端联系这么紧密,肯定是需要前后端实现一起看才能弄清楚实现的。
这就是一个典型的例子,我们是需要一个模型图来描述我们的逻辑实现的。如果我们在设计之初就构建好了这样的模型图,那么后人在理解起来就容易的多了。
当然我在实现的时候是有对它进行建模的,以下是我当时的模型图,大家可以体会一下模型图的作用。
写在最后
本篇文章在DDD的基础上,讨论了如何利用模型来驱动开发。本文意旨通过模型来反应我们的实现代码,当我们将要对代码发生变动时,我们要先将对应的模型实现,然后再去实现代码。
值得注意的是,模型驱动开发跟TDD有点像,但是它也是一个双向验证的关系,我们是可以通过模型图来反应代码设计的,但是开发过程中,模型图并不是一成不变的,我们有可能会发现一些之前在建模过程中没有考虑到的细节,这时我们需要对现有模型进行调整,然后再进行开发。不过我们要保证在建模期间尽可能的将所有场景都考虑清楚,就像我们开发某一功能前,拆分task一样。
以上就是我有关模型驱动开发的一些观点,大家如有什么观点,欢迎讨论。