产品代码都给你看了,可别再说不会DDD(四):代码工程结构
阅读原文时间:2023年08月27日阅读:2

这是一个讲解DDD落地的文章系列,作者是《实现领域驱动设计》的译者滕云。本文章系列以一个真实的并已成功上线的软件项目——码如云https://www.mryqr.com)为例,系统性地讲解DDD在落地实施过程中的各种典型实践,以及在面临实际业务场景时的诸多取舍。

本系列包含以下文章:

  1. DDD入门
  2. DDD概念大白话
  3. 战略设计
  4. 代码工程结构(本文)
  5. 请求处理流程
  6. 聚合根与资源库
  7. 实体与值对象
  8. 应用服务与领域服务
  9. 领域事件
  10. CQRS

案例项目介绍

既然DDD是“领域”驱动,那么我们便不能抛开业务而只讲技术,为此让我们先从业务上了解一下贯穿本文章系列的案例项目 —— 码如云(不是马云,也不是码云)。如你已经在本系列的其他文章中了解过该案例,可跳过。

码如云是一个基于二维码的一物一码管理平台,可以为每一件“物品”生成一个二维码,并以该二维码为入口展开对“物品”的相关操作,典型的应用场景包括固定资产管理、设备巡检以及物品标签等。

在使用码如云时,首先需要创建一个应用(App),一个应用包含了多个页面(Page),也可称为表单,一个页面又可以包含多个控件(Control),比如单选框控件。应用创建好后,可在应用下创建多个实例(QR)用于表示被管理的对象(比如机器设备)。每个实例均对应一个二维码,手机扫码便可对实例进行相应操作,比如查看实例相关信息或者填写页面表单等,对表单的一次填写称为提交(Submission);更多概念请参考码如云术语

在技术上,码如云是一个无代码平台,包含了表单引擎、审批流程和数据报表等多个功能模块。码如云全程采用DDD完成开发,其后端技术栈主要有Java、Spring Boot和MongoDB等。

码如云的源代码是开源的,可以通过以下方式访问:

码如云源代码:https://github.com/mryqr-com/mry-backend

代码工程结构

码如云,我们经常会受邀去给其他公司或组织分享DDD的落地实践经验,分享期间听众一般会问很多问题,被问得最多的反倒不是限界上下文如何划分,聚合如何设计等DDD重点议题,而是DDD工程结构该怎么搭,包该怎么分这些实实在在的问题。

事实上,DDD并未对工程结构做出要求,在码如云,我们结合行业通用实践以及自身对DDD的认识搭建出了一套适合于自身的工程结构,我们认为对于多数项目也是适用的,在本文中,我们将对此做详细讲解。

在上一篇战略设计中我们提到,码如云是一个单体项目,其通过Java分包的方式划分出了3个限界上下文,即3个模块。对于正在搞微服务的读者来说,可不要被“单体”二字吓跑了,本文所讲解的绝大多数内容既适合于单体,也适合于微服务。

以上是码如云工程的目录结构,在根分包src/main/java/com/mryqr下,分出了coreintegrationmanagement3个模块包,分别对应“核心上下文”、“集成上下文”和“后台管理上下文”,对于微服务系统来说,这3个分包则不存在,因为每个分包都有自己单独的微服务项目,也即DDD的限界上下文和微服务存在一一对应的关系。与这3个模块包同级的还有一个common包,该包并不是一个业务模块,而是所有模块所共享的一些基础设施,比如Spring的配置、邮件发送机制等。在src目录下,还包含testapiTestgatling三个目录,分别对应单元测试,API测试和性能测试代码。此外,deploy目录用于存放与部署相关的文件,doc目录用于存放项目文档,gradle目录则用于存放各种Gradle配置文件。

在以上提及的各种模块包中,程序员们最为关注的估计是core包之下应该如何进一步分包了,因为core是整个项目的核心业务模块。

在做分包时,一个最常见的反模式是将技术分包作为上层分包,然后在各技术分包下再划分业务包。DDD社区更加推崇的分包方式是“先业务,后技术”,即上层包先按照业务进行划分,然后在各个业务包内部可以再按照技术分包。

在码如云的core模块包中,首先是基于业务的分包,包含app、 assignment等几十个包,其中的app对应于应用聚合根,而assignment对应于任务聚合根,也即每一个业务分包对应一个聚合根。在每个业务分包下再做技术分包,其中包含以下子分包:

  • command:用于存放应用服务以及命令对象等,更多相关内容请参考应用服务与领域服务
  • domain:用于存放所有领域模型,更多相关内容请参考聚合根与资源库
  • eventhandler:用于存放领域事件处理器,更多相关内容请参考领域事件;
  • infrastructure:用于存放技术基础设施,比如对数据库的访问实现等;
  • query:用于存放查询逻辑,更多相关内容请参考CQRS

在这些分包下,可以根据实际情况进一步分包。

这种“先业务,后技术”的分包方式有以下好处:

  • 业务直观:所有的业务模块被放在一起,并且处于一个分包级别中,让人一眼即可全景式地了解一个软件项目中的所有业务。事实上,Robert C. Martin(Bob大叔)提出了一个概念叫尖叫架构(Screaming Architecture)讲的就是这个意思。尖叫即“哇的一声”的意思,比如当你看到一栋房子时,你会说“哇,好一栋漂亮的房子!”,也即你一眼就能识别出这是一套房子。
  • 便于导航:当你要查找一个功能时,你首先想到的一定是该功能属于哪个业务板块,而不是属于哪个Controller,因此你可以先找到业务分包,然后顺藤摸瓜找到相应的功能代码。
  • 便于迁移:每一个业务包都包含了从业务到技术的所有代码,因此在迁移时只需整体挪动业务包即可,比如,如果码如云以后要迁移到微服务架构,那么只需将需要迁出的业务包整体拷贝到新的工程中即可。

在以上子分包中,domain分包应是最大的一个分包,因为其中包含了所有的领域模型以及业务逻辑。在码如云项目的app业务包下,各个子分包所包含的代码量统计如下:

可以看到,domain包中所包含的代码量远远超过其他所有分包的总和。当然,我们并不是说所有DDD项目都需要满足这一点,而是强调在DDD中领域模型应该是代码的主体。

接下来,让我们来看看各个子分包中都包含哪些内容,首先来看domain分包:

domain分包中,最重要的当属App聚合根了,除此之外还包含领域服务AppDomainService,工厂AppFactory和资源库AppRepository。这里的AppRepository是一个接口,其实现在infrastructure分包中。基于内聚原则,有些密切联系的类被放置在了下一级子分包中,比如attributepage分包等。值得一提的是,用于存放领域事件的event包也被放置在了domain下,因为领域事件也是领域模型的一部分,不过领域事件的处理器类则放在了与domain同级的eventhandler包中,我们将在 领域事件中对此做详细讲解。

command包用于放置应用服务以及请求数据类,这里的“command”即CQRS中的“C”,表示外界向软件系统所发起的一次命令。

command包中,应用服务AppCommandService用于接收外界的业务请求(命令)。AppCommandService接收的输入参数为Command对象(以“Command”为后缀),Command对象通过其名称表达业务意图,比如CopyAppCommand用于拷贝应用(这里的“应用”表示业务上的应用聚合根),CreateAppCommand用于新建应用

eventhandler用于存放领域事件的处理器类,这些类的地位相当于应用服务,它们并不是领域模型的一部分,只是与应用服务相似起编排协调作用。

infrastructure用于存放基础设施类,主要包含资源库的实现类:

query用于存放与数据查询相关的类,这里的"query"也即CQRS中的“Q”,我们将在本系列的CQRS中对此做详细讲解。

自动化测试包含单元测试、API测试和性能测试。在API测试中,数据库和消息队列等基础设施均通过本地Docker完成搭建,测试时先启动整个Spring进程,然后模拟前端向各个API发送真实业务请求,最后验证返回结果,如果遇到有需访问第三方系统的情况,则通过Stub类进行代替。码如云采用的是“API测试为主,单元测试为辅”的测试策略,其API测试覆盖率达到了90%,所有的业务用例和重要分支都有API测试覆盖,单元测试主要用于测试领域模型,对于诸如应用服务、Controller以及事件处理器等结构性设施则不作单元测试要求,因为这些类并不包含太多逻辑,对这些类的测试可以消化在API测试中。

总结

本文主要讲解了DDD代码工程的典型目录结构,我们推荐通过“先业务,后技术”的方式进行分包,这样使得项目所体现的业务更加的直观。此外,在DDD项目中,领域模型应该是整个项目的主体,所有的领域对象和业务逻辑均应该包含在domain包下。在下文请求处理流程中,我们将对DDD项目中请求处理的全流程进行详细讲解。