DDD之代码架构

点击↑上方↑蓝色“编了个程”关注我~

荒腔走板

这是一篇迟到的文章。这其实是我写DDD的第四篇文章。去年11月份左右我在个人 站上写了三篇关于DDD的文章,都是比较偏战略部分的。那个时候我还在一个正在使用DDD的项目上,也是我第一次真正开始深入使用DDD。

2012年,Bob大叔发文,说:我总结了之前的各种架构,比如六边形架构、洋葱架构、尖叫架构(他自己2011年提出的)等等,总结出了它们好像说的其实是一个东西,我把它们抽象整合一下,提出这样一个“整洁架构”。

?

整洁架构链接:https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html

?

这样Use Cases层就不用依赖外部的层了。同样的道理,也可用于对数据库、第三方接口等的交互场景。

代码

No Code, No BB, Show me the code!

我给大家找了一个Go语音版本的整洁架构代码实现,来一起分析一下它的结构。

?

Github地址:https://github.com/manakuro/golang-clean-architecture

文章地址:https://medium.com/@manakuro/clean-architecture-with-go-bce409427d31

?

首先看一下整体的结构:

Entities层没什么好说的,就是放的领域模型。Use Cases层定义了presenter和repository的接口。并且在interactor中,使用了这两个接口。

然后在Interface Adapters层,实现了上面定义的两个接口。在user_controller.go里面,调用user_interactor。

DDD与整洁架构

看了上面的整洁架构,发现是不是还是不能跟DDD的战术模式完全match上p>

是的,DDD是用来解决软件的复杂性的,而真实的软件远比上面的demo代码复杂得多。其中两个很重要的架构上的概念CQRS、Event Sourcing、Domain Service等并没有在上面整洁架构的图中体现出来。

我也是疑惑了许久,最终在hgraca的博客里找到了答案。全部都在这张图上了:

然后再往外,是大红色轮廓包起来的Application Core。这一层定义了很多接口(也可以说是端口),比如持久化、第三方服务、搜索、CQ总线、事件总线等等。当然,也接收处理命令和查询。

思考,我曾遇到的问题

在之前的DDD项目上,我曾经遇到过一些战术模式上的问题,现在回过头去思考一下。

究竟什么时候需要领域服务h3>

可以一句话总结:当只使用领域模型做不到的时候。那两者都是在领域层,大家都不依赖外面的东西,什么情况下领域模型做不到,领域服务就做得到p>

一个很常见的场景是创建模型的时候有业务逻辑。虽然创建模型通常是放在Factory里面,但Factory里面并不适合放业务逻辑。而这个时候领域模型还没有创建,自然就只能放在领域服务里面了。

当然了,DDD最佳实践是希望能够尽量消灭领域服务层,全部内聚在模型层,只是很难达到这种完美的情况。

在应用层需要查询其它数据怎么办h3>

有时候我们可能不止是需要当前这个聚合根的数据,可能还需要其它的数据。这个读取操作当然不可能放到领域层去做,通常把它放在应用层。但应用层通常是一个聚合根对应一个ApplicationService,正常的流程是调用Repository接口获取一个领域模型对象,然后对它进行操作,再保存回数据库。

那如果需要获取其它数据怎么办呢是可能与当前领域模型无关的数据,比如“最近评论时间”。

我们团队之前的解决方案是用了一个Query,Query的职责是去数据库查询,它可以查询部分字段。但为了防止它被滥用,团队规定它只能用于repsenter或者applicationService中“为了写的读”。

回过头去看整洁架构,这里的Query其实就是User Case Output Port。不过当时我们团队的Repository和Query都没有做抽象处理,也就是说并没有依赖反转,所以ApplicationService层依赖了它们,这与整洁架构的“单向依赖”不太相符,这里其实用上依赖反转会好一点。

领域事件究竟该在什么时候发送和接收h3>

这个也是一个比较有争议的点。首先看领域事件在什么时候创建,有人认为应该在领域层创建,有人认为应该在应用层创建。个人认为在领域层创建比较好,因为创建领域事件其实也算是一种业务逻辑,并且只是创建一个领域事件的话,不会依赖任何外部的东西,放在领域层没有什么问题。

那什么时候发送领域事件呢整洁架构的规则,不应该在领域层发送,因为事件总线(或者事件发送器的实现)在最外层,如果在领域层发送,虽然有依赖倒置,但感觉也跨越太多层次了,不是一个好的实践。

那在应用层发送事件是一个比较好的方案。我们团队之前的方案比较奇葩,是在Repository的实现里面发送的。有点忘记当时为啥这样做了,不过现在看来,应该在应用层去做更合适一些。

事件接收的话,就像上面的图中所示的一样,放在应用层比较好(但在App Services外)。然后可以通过ApplicationService去完成业务逻辑。

跨聚合根的事务问题h3>

这个其实很难保证强一致的事务了,因为跨聚合根应该使用事件通信,但事件的实现方式有多种,如果是异步,那就保证不了强一致的事务。只能用一些技术手段去尽量保证最终一致。

怎样保证架构不被腐化h3>

根据之前的实践经验来看,代码架构是有可能随着时间腐化的。比如我前面提到的Query就是一个例子,得通过一些团队间的“共识”来保证它不被滥用。我们不能保证代码完全不被腐化,但是可以通过一些手段去保证依赖的层次不被腐化。

比较推荐的是使用maven/gradle的模块化,因为模块之间是有依赖关系的,只要我们不去改依赖的配置,就永远是单向依赖的。具体来说,我们可以把整洁架构上面的层级分成一个个模块,然后在配置文件里面定义它们的依赖关系。比如应用层模块,依赖领域层模块;接口和适配层模块依赖应用层模块和领域层模块。

永远的难题:定义合适的聚合根

最后谈一个比较难的问题,就是找聚合根。其实大家可能觉得,定义一个聚合根应该很简单,根据业务来就是了,比如用户、订单、商品、库存等等。但有时候我们很容易把聚合根定义得很大,因为无论聚合根多大,它都能够很好地解释。有个建模界的笑话是:我定义了一个“宇宙类”,它可以包含所有模型。

聚合根太大可能会有问题,比如代码过多、测试用例过多、性能不好等等。很有可能做着做着一个聚合根就膨胀了,这个时候你们尝试去拆分它,会发现并非像最开始现象的那样不能拆分。只是要找到合适的“借口”,要拆得有理有据。但总而言之,这是一件很难的事情。

加个星标可以第一时间看到最新文章

关注在看,评论转发,

感谢支持!

推荐阅读

  • Spring Cache,从入门到真香

  • 我是如何把一个15分钟的程序优化到了10秒的

  • InnoDB的行锁,原来为你做了这么多!

文章知识点与官方知识档案匹配,可进一步学习相关知识Java技能树使用JDBC操作数据库数据库操作92925 人正在系统学习中

声明:本站部分文章及图片源自用户投稿,如本站任何资料有侵权请您尽早请联系jinwei@zod.com.cn进行处理,非常感谢!

上一篇 2020年9月18日
下一篇 2020年9月18日

相关推荐