这是一个讲解DDD落地的文章系列,它以一个真实的并已成功上线的软件项目为例,系统性地讲解DDD在落地实施过程中的典型实践,以及在面临实际业务场景时的各种取舍。
本系列包含以下文章:
既然DDD是“领域”驱动,那么我们便不能抛开业务讲技术,为此让我们先从业务上了解一下贯穿本文章系列的案例项目 —— 码如云(不是马云,也不是码云)。如你已经在本系列的其他文章中了解过该案例,可跳过。
码如云是一个针对二维码场景应用的SaaS软件平台,采用“一物一码”的业务模式,可以为每一件“物品”生成一个二维码,并以该二维码为入口展开对“物品”的相关操作,典型的应用场景包括固定资产管理、设备巡检以及物品标签等。
在使用码如云时,首先需要创建一个应用,一个应用(App)包含了多个页面(Page)(也可称为表单),一个页面又可以包含多个控件(Control)(比如单选框控件)。应用创建好后,可在应用下创建多个实例(QR)用于表示被管理的对象(比如机器设备)。每个实例均对应一个二维码,手机扫码便可对实例进行相应操作,比如查看实例相关信息或者填写页面表单等,对表单的一次填写称为提交(Submission);更多概念请参考码如云术语。
在技术上,码如云是一个无代码平台,包含了表单引擎、审批流程和数据报表等多个功能模块。码如云全程采用DDD完成开发,其后端技术栈主要有Java、Spring Boot和MongoDB等。
在码如云,我们经常会受邀去给其他公司或组织分享DDD的落地实践经验,分享期间听众一般会问很多问题,被问得最多的反倒不是限界上下文如何划分,聚合如何设计等DDD重点议题,而是DDD工程结构该怎么搭,包该怎么分这些实实在在的问题。
事实上,DDD并未对工程结构做出要求,在码如云,我们结合行业通用实践以及自身对DDD的认识搭建出了一套适合于自身的工程结构,我们认为对于多数项目也是适用的,在本文中,我们将对此做详细讲解。
在上一篇战略设计中我们提到,码如云是一个单体项目,其通过Java分包的方式划分出了3个限界上下文,即3个模块。对于正在搞微服务的读者来说,可不要被“单体”二字吓跑了,本文所讲解的绝大多数内容既适合于单体,也适合于微服务。
以上是码如云工程的目录结构,在根分包src/main/java/com/mryqr下,分出了core、integration和management3个模块包,分别对应“核心上下文”、“集成上下文”和“后台管理上下文”,对于微服务系统来说,这3个分包则不存在,因为每个分包都有自己单独的微服务项目,也即DDD的限界上下文和微服务存在一一对应的关系。与这3个模块包同级的还有一个common包,该包并不是一个业务模块,而是所有模块所共享的一些基础设施,比如Spring的配置、邮件发送机制等。在src目录下,还包含test、apiTest和gatling三个目录,分别对应单元测试,API测试和性能测试代码。此外,deploy目录用于存放与部署相关的文件,doc目录用于存放项目文档,gradle目录则用于存放各种Gradle配置文件。
在以上提及的各种模块包中,程序员们最为关注的估计是core包之下应该如何进一步分包了,因为core是整个项目的核心业务模块。
在做分包时,一个最常见的反模式是将技术分包作为上层分包,然后在各技术分包下再划分业务包。DDD社区更加推崇的分包方式是“先业务,后技术”,即上层包先按照业务进行划分,然后在各个业务包内部可以再按照技术分包。
在码如云的core模块包中,首先是基于业务的分包,包含app、 assignment等几十个包,其中的app对应于应用聚合根,而assignment对应于任务聚合根,也即每一个业务分包对应一个聚合根。在每个业务分包下再做技术分包,其中包含以下子分包:
在这些分包下,可以根据实际情况进一步分包。
这种“先业务,后技术”的分包方式有以下好处:
在以上子分包中,domain分包应是最大的一个分包,因为其中包含了所有的领域模型以及业务逻辑。在码如云项目的app业务包下,各个子分包所包含的代码量统计如下:
可以看到,domain包中所包含的代码量远远超过其他所有分包的总和。当然,我们并不是说所有DDD项目都需要满足这一点,而是强调在DDD中领域模型应该是代码的主体。
接下来,让我们来看看各个子分包中都包含哪些内容,首先来看domain分包:
在domain分包中,最重要的当属App聚合根了,除此之外还包含领域服务AppDomainService,工厂AppFactory和资源库AppRepository。这里的AppRepository是一个接口,其实现在infrastructure分包中。基于内聚原则,有些密切联系的类被放置在了下一级子分包中,比如attribute和page分包等。值得一提的是,用于存放领域事件的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项目中请求处理的全流程进行详细讲解。
更新时间:2024-08-27
本站资料均由网友自行发布提供,仅用于学习交流。如有版权问题,请与我联系,QQ:4156828
© CopyRight 2020-2024 All Rights Reserved. Powered By 71396.com 闽ICP备11008920号-4
闽公网安备35020302034903号