本专栏 将通过以下几块内容来搭建一个 模块化:可以根据项目的功能需求和体量进行任意模块的组合或扩展 的后端服务
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
中的概念看是否能够解决当前项目存在的问题
如果某个概念确实能够解决问题,那就拿过来用
如果某个概念反而让项目更复杂了,那就扔了吧