本专栏 将通过以下几块内容来搭建一个 模块化:可以根据项目的功能需求和体量进行任意模块的组合或扩展 的后端服务
v2.0:结合DDD与MVC的中庸之道(启发与思路)(本文)
v2.0:结合DDD与MVC的中庸之道(标准与实现)
v2.0:结合DDD与MVC的中庸之道(优化与插件)
未完待续......
在之前的文章 v2.0:项目结构优化升级 中,我们将项目的结构进行了优化,优化之后的项目结构如下

简单说明
考虑到可能有读者是第一次看这个专栏,所以我还是先简单介绍一下,详细的内容大家可以看我之前的专栏文章
这里模块化的设想其实就是可插拔的功能模块,如果需要这个功能,就把这个模块用Gradle或Maven的方式编译进来,如果不需要,去掉对应的依赖就行了,避免改动代码,因为一旦涉及到代码改动的话,就会变得"改不断,理还乱"
当然了,需要达到每个模块都能够任意拆卸的程度其实并不简单,所以我借鉴了DDD的思想来达成这个目的,专栏中也有这一块的内容,今天这篇文章就是对DDD的进一步优化
当我们把所有模块都编译进来的时候,那么就是一个单体应用,如果把多个模块分开编译,那么就变成了微服务
以我们juejin项目的三个模块(用户,沸点,通知)为例:
在最开始的时候,我们可以将这三个模块打包成一个服务,作为单体应用来运行,适合项目前期体量较小的情况
之后,当我们发现沸点的流量越来越大,就可以将沸点模块拆分出来作为单独的一个服务方便扩容,用户模块和通知模块打包在一起作为另一个服务,这样就从单体应用变成了微服务
注:从单体应用到微服务的切换是不需要修改代码的
DDD并不完美
DDD在理论上其实是近乎完美的
但是当你尝试实现的时候你就会发现好像有些地方很别扭
毕竟书中也只是非常简单的例子
和我们平时的需求相比还是过于理想化
我在实现juejin的沸点时就遇到了这样一个问题
当一条沸点的评论达到10w的时候
沸点模型
java
public class Pin {
private String id;
//沸点内容
private String content;
//沸点作者
private User user;
//沸点评论
private List<Comment> comments;
//省略其他属性和方法
}
查询沸点
java
//根据 id 获得沸点
Pin pin = pinRepository.get(pinId);
//获得沸点的评论
List<Comment> comments = pin.getComments();
不需要通过数据库再查询一步的操作,直接就能拿到,是不是很简单?
但问题是我们也不是每次查询都需要评论数据
不管要不要,每次都先查询10w条数据,这谁顶得住
添加评论
java
//根据 id 获得沸点
Pin pin = pinRepository.get(pinId);
//添加评论
pin.addComment(newComment);
//更新沸点
pinRepository.update(pin);
这咋更新?10w条评论啥都没变再往数据库写一遍?
所以我们现在出现了2个问题
问题一:查询的时候会把不需要查询的数据也查询出来
问题二:更新的时候会把不需要更新的数据也更新一遍
延迟加载
查询的时候会把不需要查询的数据也查询出来
最简单的办法,需要的时候再查询
我们可以这样定义沸点类和评论类
java
public class Pin {
private String id;
//沸点内容
private String content;
//沸点作者
private User user;
//沸点评论
private Comments comments;
//省略其他属性和方法
}
public class Comments {
//沸点 id
private String pinId;
//评论存储
private commentRepository;
public List<Comment> list() {
return commentRepository.listByPinId(pinId);
}
//获得最新的 n 条评论
public List<Comment> listNewest(int n) {
return commentRepository.listNewestByPinId(pinId, n);
}
//省略其他属性和方法
}
这样只有当我们需要评论的数据的时候才会加载
java
//根据 id 获得沸点,不会加载评论数据
Pin pin = pinRepository.get(pinId);
//获得沸点的评论,调用 list 才加载
List<Comment> comments = pin.getComments().list();
扔掉聚合根
更新的时候会把不需要更新的数据也更新一遍
一种方式是,额外加个标签,说明评论是新加的
然后再存库的时候遍历所有的评论根据标签来新增或是不处理
或者是搞个快照,根据前后的差别对比来新增或是不处理
但是总觉得为了强行DDD搞的那么麻烦好像不太值
这不给我自己增加工作量吗
为什么不学学MVC想改哪条改哪条
java
//根据 id 获得沸点
Pin pin = pinRepository.get(pinId);
//生成评论
Comment comment = create(pin, 接口获得的评论数据);
//添加评论
commentRepository.create(comment);
这不啥问题都解决了吗,非得让司机绕远路
中庸之道
可能有读者会有疑问
你这样改的话和MVC有啥区别呢,为什么不直接用MVC?
当然还是有区别的
我们需要的就是让DDD和MVC优势互补
建模一致性
首先当我们根据业务本身来建模的时候
不同的人建立的模型基本一致,这非常重要
比如沸点的业务模型
json
{
"id": "p1",
"content": "沸点内容",
"user": {
//沸点作者
"id": "u1",
"name": "u1"
},
"comments": [{
"id": "c1",
"content": "评论内容",
"user": {
//评论作者
"id": "u2",
"name": "u2"
}
}]
}
不管技术怎么样,基本上所有的人设计的都和这差不多,最多就是字段名不太一样
所以只要业务理解了,看别人设计的模型也很容易理解,所有人的逻辑思路就会趋于一致
从沟通的角度来说就会非常方便,毕竟你不需要先解释某一个表字段是用来干什么的,有的时候解释之后别人还不一定能听懂
业务内聚性
当我们使用领域模型来处理业务的时候
我们可以直接拿到对应的模型和数据
java
//根据 id 获得沸点
Pin pin = pinRepository.get(pinId);
//不需要显式的根据 pinId 去查询数据库
//获得沸点的评论
List<Comment> comments = pin.getComments().list();
我们不需要去思考沸点和评论在数据库表中的关联关系,用哪个类去查询,查询条件是什么,避免不必要的时间成本
也就是说我们的业务中不会有数据库的相关操作,数据库这部分的技术层和业务逻辑完全没有耦合
这要是写起来不得一气呵成?
存储扩展性
当技术和业务没有耦合的时候
我们可以使用DDD中的六边形架构以一种可插拔的方式来实现数据库存储
java
public interface PinRepository {
void create(Pin pin);
Pin get(String id);
//省略其他方法
}
public abstract class AbstractPinRepository implements PinRepository {
@Override
public void create(Pin pin) {
doCreate(do2po(pin));
}
@Override
public Pin get(String id) {
return po2do(doGet(id));
}
//写数据库
public abstract void doCreate(PinPO po);
//读数据库
public abstract PinPO doGet(String id);
//领域模型转数据模型
public abstract PinPO do2po(Pin pin);
//数据模型转领域模型
public abstract Pin po2do(PinPO po);
//省略其他方法
}
这个时候,数据库只需要最基础的增删改查
我们可以非常方便的替换PinRepository
当数据库表设计的不合理的时候只需要重新实现一个PinRepository并重写do2po和po2do就行了
当想要尝试其他的持久层框架的时候只需要重新实现一个PinRepository并重写doCreate和doGet就行了
对业务逻辑完全没有任何影响,甚至方便我们尝试体验各种技术
表设计的有问题?嗨,重新设计一下重写两个方法不分分钟的事
总结
其实在落地DDD的过程中我有一个思路转变的过程
一开始的时候,我把DDD奉为圣经,就好像拿到了解决所有问题的钥匙
但是当我真的按照DDD一步一步落实的时候才发现,只有在服务器永不断电并且内存无限大的情况下,DDD才是真正的最优解
所以现实情况下,DDD其实是用了一些概念去弥补另一些概念所产生的问题
以至于存在一些看似十分抽象的解决方案
当然这并没有什么问题,毕竟我们平时解决问题的时候也经常出现这样的情况
在DDD落地搁置了一段时间后我开始转变思路
从原来只是盯着DDD到后来从项目本身出发
参考DDD中的概念看是否能够解决当前项目存在的问题
如果某个概念确实能够解决问题,那就拿过来用
如果某个概念反而让项目更复杂了,那就扔了吧