在单体应用的一些DDD实践经验
阅读此文需要一定的DDD基础,如果你是第一次接触DDD读者,建议先去阅读一些DDD相关的书籍或者文章之后再来阅读本文。
背景
自从我在团队中推行DDD以来,我们团队经历了一系列的磨难——先是把核心项目重构,接着又在一些衍生项目中尝试全面落地DDD, 最终探索了一些经验出来,特此记录一下。
本文采用语言无关的角度陈述,无论你是Java或者c#的开发同学相信都可以无障碍阅读。
请注意本文并不是介绍如何实现DDD,因为这个话题实在太大了。
这次的主题是分享一些我们团队在实践DDD过程中碰到问题和如何克服它们,以及介绍一下我们所使用的架构体系。
先说说为什么标题限定在“单体应用”这个范围内,
- 我们团队这次实践的应用全是单体应用
如果是分布式的应用,那么拆分限界上下文(BoundedContext)的最佳实践是什么?当然是微服务!
我相信现在讨论微服务的文章肯定不在少数,微软也专门出过容器化微服务架构的电子书。传送门点我。
资源如此丰富,当然就不需要我画蛇添足了。
领域模型
领域模型的分析可以说是DDD当中最为核心的部分,因为你整个系统的业务逻辑代码都是基于领域模型而构成的。
而要将业务逻辑转换成领域模型除了对业务的熟悉外还需要极高的抽象能力,所以一般需要业务专家和建模专家共同完成。
怎样提炼一个好的领域模型是一个非常大的话题,推荐你阅读以下书籍:
- 《领域驱动设计:软件核心复杂性应对之道》Eric Evans
- 《实现领域驱动设计》Vaughn Vernon
- 《领域驱动设计与模式实战》Jimmy Nilsson
另外微软架构电子书上还有推荐其他几本DDD的书籍,遗憾的是,JD和TB都没搜到。
在团队刚开始分析领域模型时,对所有相关者都是一个极大的挑战,我这里分享几点经验帮助团队更好地度过这段时期:
- 不要想着能够一次提炼出完美的领域模型(除非团队中有着经验丰富的DDD实践者),通常来说,我们会在会议上决定一个粗略的模型,然后在开发过程中你会发现有一些不自然的地方,比如某些上下文频繁地与其他上文通信,或者某个实体的行为不是很恰当,这个时候再去修正领域模型,这样演进式的过程可以大大降低你们在初期的压力。
- 如果你的团队整体能力不足以支撑领域模型的推行,或者他们在初期的配合度不高时,你可以选择把你的项目中业务逻辑最为复杂的部分使用弱化的领域模型拆解,比如仅使用充血模型和领域服务,这样至少你可以对最为复杂的部分引入一些DDD战术模式或设计模式。
- 就算你的团队能力够了,但大部分人都没有DDD的经验的话,我也建议先只引入部分模式(比如只引入实体,值对象和仓储这类比较容易理解的模式)来提高团队的敏感度之后再采用完整的领域模型。
- 领域模型会对查询带来一定的复杂性,这种时候你可以采用CQRS来分离Query和Command,只有在Cammand的时候你才需要发挥领域模型的威力,至于Query,SQL语句显然是更好选择。
基础架构
了解DDD的同学都应该知道,DDD当中最为重要的部分就是限界上下文(BoundedContext),在领域模型中我们区分好了上下文之后,下一步就是选择一种技术手段来确保每个上下都是低耦合高内聚且自治的。
在分布式应用中,多数设计者和包括微软架构的电子书都会推荐使用一个上下文对应一个微服务的方式来实现(确实微服务和上下文的设计需求不谋而合)。
但单体应用该怎么办呢?
有同学说,我们可以通过命名空间来隔离它们啊。
不错,我们可以这样做,但是有以下几个缺点
- 在使用IDE的智能引用时,你得确认你引用的实体究竟是位于当前上下文之内还是之外。
- 会导致你的项目结构层次过深,不便于查看。(至于过深的标准是多少,看个人了,对于我来说,5层是可以接受的上限,理想是控制在4层以内)
- 不便于向微服务架构迁移
所以我们选择了使用程序集(java是使用jar包)的方式来隔离每个上下文,这样做克服了以上的缺点,但却带来了新的问题:动态加载这些上下文。
不过这种程度的问题比起带来的收益几乎可以忽视。
我们团队使用一个基础平台来动态加载这些上下文,
我们采用了 Abp 框架提供的插件功能来实现,如果你也是.net 的使用者,也可以采用 Abp 来构建这个应用。
当然自己写一个动态加载功能也并不困难。
基础架构如下图所示:
可是我们的平台要承担很多功能,比如开放RESTful的API与Webservice(为了兼容老的接口), 同时还要提供授权(使用了基于Oauth2.0协议的三种模式)、数据库初始化、处理请求上下文等等,我就不一一列出来了。
我们希望BC(BoundedContext,后文都会简写为BC)里不需要关注网络层面的东西而只聚焦于应用,所以很多通用的事情都由平台来承担, 而且有时还会有一些交互,比如在验证权限时你得跟用户权限上下文通信。
在这种前提下,我们抽出了一个用于连接平台和这些BC的交互层,我们把它称作——桥接组件(BrigeComponent),它负责联系起平台和这些BC,外加上一些共用的基础设施,我们的架构图变成了这样:
这样一来,你可以把每个BC都当作微服务来处理,每一个BC内的分层结构你可以按你的喜欢的来,如果你喜欢标准的三层架构(UI + BLL + DAL),你可以将BC设计那样。
你甚至可以每个BC都采用不同的风格,比如一个采用N层架构,而另一个采用事件驱动架构(EDA)。
这里我们的BC都用了相同的DDD推荐分层架构(这里省去了 表现层, 因为现代应用大多都是前后端分离了的),如下图所示:
好了,现在整体架构和领域模型都已经确定下来后,我们开始编码了,但很快我们就遇到了阻碍。
“结算上下文需要访问用户权限上下文,它需要知道这个用户的机构信息,我可以直接引用吗?”
“帐户上下文这里输出的数据需要通用上下文提供一些有效性校验,我可以直接引用吗?”
“我这里也需要访问通用上下文!”
……
好吧,如果我们直接提供引用,会有以下问题:
- 由于我们采用了程序集分割上下文,所以相互引用是不被允许的。
- 就算克服了相互引用的问题,最终也会导致引用拓扑图混乱不堪。
- 强耦合,这会直接影响到以后的拓展性。
在微服务中,为了克服服务间的互相通信问题,目前我了解的有两类解决方案,
一是类似于ESB(企业服务总线)的中心化通信模式,比如大名鼎鼎的SprinCloud。
二是现在微服务界炒得沸沸腾腾的ServiceMesh(服务网格),比如 Linkerd 和 Istio。
我们项目选择了前者,使用了类似于ESB中心化通信方式来解决,简单来说,你需要一个通信中介者(Mediator)来负责BC之间的交互,结构图如下:
如果你是 .Net 的开发者,请容许我给你安利一下我们在项目中使用的,自己开发的组件——ServiceAnt,它目前只支持进程内的通信,但不久后会开发分布式的。
详细情况你可以点击上面的连接进去查看,也可以查看我写的 另一篇博客 了解ServiceAnt是做什么的,当然你也可以选择 Mediator 来实现这个通信中间件。
Java的话,由于经验较少,没有发现类似的项目,Mule ESB什么的就跟 NServiceBus 一样是重量级的组件,不适用我们这样的场景。
以上就是我们用于实现DDD的基础架构,基于这样的架构我们可以很轻松地将现有应用向微服务拆分。
当然,上面的架构隐藏了很多细节,比如大量的基础设施(Ioc,Aop, Logger, cache等等),
原因之一是因为这些东西的设计都很常见,网上你随便就可以搜到相关设计的文章,
原因之二是因为我不想这些细节影响到了读者的关注点,我希望我们可以聚焦于如何实现DDD而不是系统的其他部分。
其他的一些话
在推行DDD过程中,总会有一些成员会问我,DDD给我们带来的好处是什么。
我总会不厌其烦地告诉他们,为了降低系统的维护成本和更合理地去解决系统业务的复杂性。
但后来我渐渐发现,实现DDD本身就不是一件容易的事情,它会对项目引入新的复杂性,有时候你会发现你团队花上大量时间去建模之后,在开发过程中却依然需要不断修正模型。
这很容易让整个团队士气变低,并且让开发人员有挫败感,这种时候我经常会怀疑DDD对我们而言是否真的有价值。
不过坚持下去,在你使用DDD完成一到两个项目之后,你会发现建模是一件非常有意思的事情——提炼业务并将其转换为一个无关技术的模型,这就跟搭积木一样。
最后给所有希望通过DDD来改善项目,并且提升自己的同学说以下两点:
1,不要奢望光通过阅读就能充分地理解DDD,你需要真正去实践(当然,框架和架构设计也是一样的,不要做象牙塔里的架构师)
2,实践的过程你总会碰见疑惑和挫折,比如完全不知道如何拆分上下文,也不知道该如何使用那些战术模式,这个时候再把那几本书拿出来翻翻,你就会发出“啊,原来这种场景还可以这样处理”的感概。
那句话怎么说来着,
The one trying to wear the crown must withstand the weight.
在单体应用的一些DDD实践经验的更多相关文章
- 领域驱动设计(DDD)的实践经验分享之ORM的思考
原文:领域驱动设计(DDD)的实践经验分享之ORM的思考 最近一直对DDD(Domain Driven Design)很感兴趣,于是去网上找了一些文章来看看,发现它确实是个好东西.于是我去买了两本关于 ...
- 领域驱动设计(DDD)的实践经验分享之持久化透明
原文:领域驱动设计(DDD)的实践经验分享之持久化透明 前一篇文章中,我谈到了领域驱动设计中,关于ORM工具该如何使用的问题.谈了很多我心里的想法,大家也对我的观点做了一些回复,或多或少让我深深感觉到 ...
- CI Weekly #6 | 再谈 Docker / CI / CD 实践经验
CI Weekly 围绕『 软件工程效率提升』 进行一系列技术内容分享,包括国内外持续集成.持续交付,持续部署.自动化测试. DevOps 等实践教程.工具与资源,以及一些工程师文化相关的程序员 Ti ...
- DDD实践切入点(二)
最近发现下面关于上下文的理解有些问题,不太好改,暂时先不改了 承前:大型系统的支撑,应用系统开发思想的变迁,DDD实践切入点(一) 从大比例结构入手已经开始了系统的建设,大家都知道需求是会不断变化不断 ...
- DDD实践案例:引入事件驱动与中间件机制来实现后台管理功能
DDD实践案例:引入事件驱动与中间件机制来实现后台管理功能 一.引言 在当前的电子商务平台中,用户下完订单之后,然后店家会在后台看到客户下的订单,然后店家可以对客户的订单进行发货操作.此时客户会在自己 ...
- DDD实践2
DDD实践切入点(二) 承前:大型系统的支撑,应用系统开发思想的变迁,DDD实践切入点(一) 从大比例结构入手已经开始了系统的建设,大家都知道需求是会不断变化不断深入的,刚开始自然是模糊的大比例结构对 ...
- DDD实践(一)
DDD实践切入点(一) 前两篇:大型系统的支撑,应用系统开发思想的变迁 之前大致说了使用DDD的前期准备,现在可以真正开始实践了,以我刚刚结束的一个简单的经典DDD方式的项目为例子,当然由于比较简单, ...
- 根据实践经验,讲述些学习Java web能少走的弯路,内容摘自java web轻量级开发面试教程
在和不少比较上进的初级程序员打交道的过程中,我们总结出了一些能帮到合格程序员尽快进阶的经验,从总体上来讲,多学.多实践不吃亏.本文来是从 java web轻量级开发面试教程从摘录的. 1 哪些知识点 ...
- 华为云对Kubernetes在Serverless Container产品落地中的实践经验
华为云容器实例服务,它基于 Kubernetes 打造,对最终用户直接提供 K8S 的 API.正如前面所说,它最大的优点是用户可以围绕 K8S 直接定义运行应用. 这里值得一提是,我们采用了全物理机 ...
随机推荐
- Tomcat与SpringMVC结合分析(一)
关键字: Bootsrap,Catalina,Server,Service,Engine,Host,Context,Wrapper,Valve,Pipeline,ContextConfig,Servl ...
- 腾讯云负载均衡CLB的那些“独门利器”
欢迎大家前往云+社区,获取更多腾讯海量技术实践干货哦~ 作者:李想 腾讯人做产品一直是很贴近用户的需求的,腾讯云也不例外.负载均衡器作为公有云上的最基础的网络服务,几乎每家云厂商都会提供,虽然负载均衡 ...
- 第十四章:Python の Web开发基础(一) HTML与CSS
本課主題 HTML 介绍 CSS 介绍 HTML 介绍 HTML 的头部份,重点: 定义HTML 的编码:<meta charset="UTF-8"/> 定义标题: & ...
- 一种基于http协议的敏感数据传输方案
最近公司需要通过公网与其它平台完成接口对接,但是基于开发时间和其它因素的考虑,本次对接无法采用https协议实现.既然不能用https协议,那就退而求其次采用http协议吧! 那么问题来了!在对接的过 ...
- Html5 移动端 触摸滑动事件
以下代码经过测试 没有问题 且可以循环滑动 <!DOCTYPE html><html xmlns="http://www.w3.org/1999/xhtml"& ...
- 【读书笔记】【深入理解ES6】#11-Promise与异步编程
异步编程的背景知识 JavaScript 引擎是基于单线程(Single-threaded)实际循环的概念构建的,同一时刻只允许一个代码块在执行. 所以需要跟踪即将运行的代码,那些代码被放在一个任务队 ...
- git 本地代码到github
一·什么是gitHub? 官网解释:gitHub是一个让无论处于何地的代码工作者能工作于同一个项目,同一个版本的平台.(GitHub is a code hosting platform for ve ...
- ConstraintLayout知识记录
一.准备工作 1. 确保SDK Tools已经下载了ContraintLayout的支持库. 2. gradle中增加对ConstraintLayout的依赖. compile 'com.andr ...
- 大话命令之--ss
大话命令之-ss ss是Socket Statistics的缩写.顾名思义,ss命令可以用来获取socket统计信息,它可以显示和netstat类似的内容. 优势: (1)显示更多更详细的有关TCP和 ...
- PIL遇到问题解决
PIL 全称:Pillow 在使用PIL4.2.1版本读取jpeg文件时,报cannot identify image file,去github源查找原因:https://github.com/pyt ...