我正在参加「掘金·启航计划」
本专栏 将通过以下几块内容来搭建一个 模块化:可以根据项目的功能需求和体量进行任意模块的组合或扩展 的后端服务
v2.0:项目结构优化升级(本文)
v2.0:项目构建+代码生成「插件篇」
v2.0:扩展模块实现技术解耦
未完待续……
在之前的文章 服务端模块化架构设计|项目结构与模块化构建思路 中,我们以掘金的部分功能为例,搭建了一个支持模块化的后端服务项目juejin
,其中包含三个模块:juejin-user(用户)
,juejin-pin(沸点)
,juejin-notification(通知)
注:juejin-notification(通知)
在之前的文章中就是juejin-message(消息)
,因为我看掘金用的是notification
,所以就统一了下
简单说明
考虑到可能有读者是第一次看这个专栏,所以我还是先简单介绍一下,更详细的思路大家可以看我之前的专栏文章
这里模块化的设想其实就是可插拔的功能模块,如果需要这个功能,就把这个模块用Gradle
或Maven
的方式编译进来,如果不需要,去掉对应的依赖就行了,避免改动代码,因为一旦涉及到代码改动的话,就会变得“改不断,理还乱”
当然了,需要达到每个模块都能够任意拆卸的程度其实并不简单,所以我借鉴了DDD
的思想来达成这个目的,专栏中也有这一块的内容,另外后续也会再写一篇针对DDD
优化的文章
所以当我们把所有模块都编译进来的时候,那么就是一个单体应用了,如果把多个模块分开编译,那么就变成了微服务
以我们juejin
项目的三个模块(用户,沸点,通知)为例:
在最开始的时候,我们可以将这三个模块打包成一个服务,作为单体应用来运行,适合项目前期体量较小的情况
之后,当我们发现沸点的流量越来越大,就可以将沸点模块拆分出来作为单独的一个服务方便扩容,用户模块和通知模块打包在一起作为另一个服务,这样就从单体应用变成了微服务
注:从单体应用到微服务的切换是不需要修改代码的
这篇文章主要是对服务端模块化架构设计|项目结构与模块化构建思路(先阅读这篇会更容理解)中的项目整体结构进行一些升级
优化之后的项目结构如下:
嗯?这项目结构也太复杂了吧。。。自己创建根本记不住!
不用担心,下一篇文章就会给大家演示怎么用插件一键生成新项目,还提供了domain
和module
的代码生成功能哦~
起因
前段时间,公司有一个小型项目需要开发,就是非常简单的一个后台服务,但是呢,作为大老板的亲儿子项目,万一后面复杂起来了,不得先未雨绸缪?
于是我决定就用模块化架构设计(对,就是本专栏的思路)来设计一个完美的服务
你看看,这不撞枪口上了吗(坏笑)
然而。。。在一开始就卡住了
虽然之前已经成功的搭建了juejin
项目,但是你让我再重新搭一个项目,不参照之前的项目还真不太好复刻
于是只能照着juejin
项目一点一点复制粘贴,因为很多名称不再是juejin
了,所以并不是直接复制过来就行,有些地方还调试了好久
不知道过了多久!项目终于算是搭完了,这里就出现了
第一个问题
就算是有参考的项目和我之前的文章,新建一个项目也过于痛苦
这就为我后面写了一个IDEA
的插件来帮忙生成项目埋下了。。。伏笔!
这里先把这个问题记下来,下一篇文章给大家具体说说插件
模型复用
接着根据各个功能创建对应的模块,但是创建到一半,我又发现了一个问题
不知道大家还记不记得,DDD领域驱动设计与业务模块化(概念与理解) 中我是这样设计的:
juejin-pin(沸点)
模块中会用到User
数据,我们可以在juejin-pin(沸点)
模块中先定义PinUser
这样的模型,然后通过接口来进行模型间的转换
我在juejin
项目的示例中分别使用com.bytedance.juejin.user.User
和com.bytedance.juejin.pin.User
来作为juejin-user(用户)
模块和juejin-pin(沸点)
模块的用户模型
进行服务间调用的时候会将com.bytedance.juejin.user.User(用户模块中的用户)
先转为UserRO(remote object)
,通过网络请求将数据传到沸点模块,再将UserRO
转为com.bytedance.juejin.pin.User(沸点模块中的用户)
而我现在的项目有5个模块,5个模块都要用到用户数据,就会出现5份一模一样的代码
并且还不是一个文件,根据 DDD领域驱动设计与业务模块化(落地与实现) 和 DDD领域驱动设计与业务模块化(优化与重构) 中的示例,我们的用户模块会包含:
User //用户模型
UserFacadeAdapter //视图与模型的转换接口
UserFacadeAdapterImpl //视图与模型的转换接口实现
UserImpl //用户模型实现
UserInstantiator //用户实例化器
UserInstantiatorImpl //用户实例化器实现
UserRepository //用户仓储
Users //用户集合
UsersImpl //用户集合实现
也就是说,这些文件的内容一模一样,仅仅只有包名不一样!
这就很难受了,如果修改用户模型导致每个模块都要改,那就要改5遍,万一之后又加了5个模块,那要修改用户模型就要改10遍
这可不能给自己挖坑,10个模块,代码复制10遍,然后修改百来个文件的包名?
你还是把我杀了吧,我咬紧牙关命令我发出这句话~
于是就出现了
第二个问题
不同模块各自定义了其他模块的内部专用模型会增大后期维护的工作量(非常大的工作量!!!)
那么既然代码内容都是一样的,只是包名的一部分不一样,那倒不如直接把模型抽出来进行复用
还是用juejin
项目来作为例子
我们可以新增一个juejin-domain-user
模块,在里面定义User
模型等一些接口
将juejin-user
改名为juejin-module-user
同时依赖juejin-domain-user
将juejin-pin
改名为juejin-module-pin
同时依赖juejin-domain-pin
相当于将原来的juejin-xxx
拆成了juejin-domain-xxx(领域模块)
和juejin-module-xxx(功能模块)
这样就不需要在juejin-module-pin(沸点)
模块中单独定义用户模型,而且其他模块也可以直接依赖juejin-domain-user
来复用用户模型
这里大家有可能会有一些疑问,我们之前为juejin-module-pin(沸点)
模块单独定义用户模型是为了避免耦合
而现在依赖外部的模型不就有可能出现模型修改的问题了吗
确实,这样的做法有可能会因为修改juejin-domain-user
而对juejin-module-pin
造成影响,但这是实践之后的取舍,原因如下:
- 解决了我们上述的问题(后续维护只需要改一遍)
- 前期直接引入比每个模块都手动创建方便很多,也减少很多工作量
- 用户模块中的
User
和UserRO
的转换需要单独实现,沸点模块中的User
和UserRO
的转换也需要单独实现,每个模块都需要单独实现一个转换器,而抽离出来之后,User
和UserRO
只需要一个转换器实现
所以综合考虑下来,将领域模型单独抽离出来更合适(实际上就是因为减少大量的重复工作,更方便!)
另外,我们可以把juejin-domain-user
看作j(uejin)dk
,引用User
和引用String
一样,对于jdk
来说,新的版本肯定要兼容旧的版本,所以juejin-domain-user
的定位和之前的juejin-user
是完全不一样的,不再是一个业务的实现,而是定义了User
的规范,成为了底层的基础依赖模块
模块分组
将juejin-user
拆成juejin-domain-user
和juejin-module-user
之后(另外两个模块也一起拆开),又出现了一个新的问题
现在我们的模块变成了这样(原来的项目结构进行拆分之后)
juejin-application-single
juejin-application-pin
juejin-application-system
juejin-basic
juejin-gateway
juejin-domain-user
juejin-domain-pin
juejin-domain-notification
juejin-module-user
juejin-module-pin
juejin-module-notification
看着好像没什么问题,但是这才3个功能模块啊
而我的项目有5个功能模块,大一点的项目有10个功能模块,每个功能模块都可能有对应的domain
,module
和application
这数量就直接乘上3了,10个功能就有30个模块,要是想找某个模块,眼睛度数不得再高上几度?
这就出现了
第三个问题
模块太多导致查找效率低下
所以我就给这些模块分了个组,变成了下面这样
juejin-application //启动模块
application-boot //单体应用
application-cloud-gateway //网关
application-cloud-pin //沸点
application-cloud-system //用户和通知
juejin-basic //基础模块
basic-boot //单体应用的基础模块
basic-cloud //微服务的基础模块
juejin-domain //领域模块
domain-notification //通知领域
domain-pin //沸点领域
domain-user //用户领域
juejin-module //功能模块
module-notification //通知功能
module-pin //沸点功能
module-user //用户功能
在二级目录没有展开的时候是这样的
juejin-application
juejin-basic
juejin-domain
juejin-module
首先分组之后,功能划分就很清晰:
application
就是可以作为服务的启动模块basic
就是通用配置的基础模块domain
就是领域模型的定义module
就是基于领域模型的功能实现
先确定要改哪一块,再点开来找就方便很多,非常清爽有木有!
PS:juejin-token
,juejin-login
和juejin-rpc
,这几个模块会在之后的扩展模块篇做详细说明
总结
所以说不管理论与数据多完美,很多事情还是要亲自实践,亲身感受,比如缺钱的话可以把闲置的房子租出去(bushi
下一篇:v2.0:项目构建+代码生成「插件篇」