服务端模块化架构设计 2.0|结合DDD与MVC的中庸之道(启发与思路)

本专栏 将通过以下几块内容来搭建一个 模块化:可以根据项目的功能需求和体量进行任意模块的组合或扩展 的后端服务

项目结构与模块化构建思路

RESTful与API设计&管理

网关路由模块化支持与条件配置

DDD领域驱动设计与业务模块化(概念与理解)

DDD领域驱动设计与业务模块化(落地与实现)

DDD领域驱动设计与业务模块化(薛定谔模型)

DDD领域驱动设计与业务模块化(优化与重构)

RPC模块化设计与分布式事务

v2.0:项目结构优化升级

v2.0:项目构建+代码生成「插件篇」

v2.0:扩展模块实现技术解耦

v2.0:结合DDD与MVC的中庸之道(启发与思路)(本文)

v2.0:结合DDD与MVC的中庸之道(标准与实现)

v2.0:结合DDD与MVC的中庸之道(优化与插件)

未完待续......

在之前的文章 v2.0:项目结构优化升级 中,我们将项目的结构进行了优化,优化之后的项目结构如下

简单说明

考虑到可能有读者是第一次看这个专栏,所以我还是先简单介绍一下,详细的内容大家可以看我之前的专栏文章

这里模块化的设想其实就是可插拔的功能模块,如果需要这个功能,就把这个模块用GradleMaven的方式编译进来,如果不需要,去掉对应的依赖就行了,避免改动代码,因为一旦涉及到代码改动的话,就会变得"改不断,理还乱"

当然了,需要达到每个模块都能够任意拆卸的程度其实并不简单,所以我借鉴了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

当然还是有区别的

我们需要的就是让DDDMVC优势互补

建模一致性

首先当我们根据业务本身来建模的时候

不同的人建立的模型基本一致,这非常重要

比如沸点的业务模型

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并重写do2popo2do就行了

当想要尝试其他的持久层框架的时候只需要重新实现一个PinRepository并重写doCreatedoGet就行了

对业务逻辑完全没有任何影响,甚至方便我们尝试体验各种技术

表设计的有问题?嗨,重新设计一下重写两个方法不分分钟的事

总结

其实在落地DDD的过程中我有一个思路转变的过程

一开始的时候,我把DDD奉为圣经,就好像拿到了解决所有问题的钥匙

但是当我真的按照DDD一步一步落实的时候才发现,只有在服务器永不断电并且内存无限大的情况下,DDD才是真正的最优解

所以现实情况下,DDD其实是用了一些概念去弥补另一些概念所产生的问题

以至于存在一些看似十分抽象的解决方案

当然这并没有什么问题,毕竟我们平时解决问题的时候也经常出现这样的情况

DDD落地搁置了一段时间后我开始转变思路

从原来只是盯着DDD到后来从项目本身出发

参考DDD中的概念看是否能够解决当前项目存在的问题

如果某个概念确实能够解决问题,那就拿过来用

如果某个概念反而让项目更复杂了,那就扔了吧

上一篇:v2.0:扩展模块实现技术解耦

相关推荐
鹿屿二向箔10 小时前
基于SSM(Spring + Spring MVC + MyBatis)框架的汽车租赁共享平台系统
spring·mvc·mybatis
假装我不帅13 小时前
asp.net framework从webform开始创建mvc项目
后端·asp.net·mvc
鹿屿二向箔15 小时前
基于SSM(Spring + Spring MVC + MyBatis)框架的咖啡馆管理系统
spring·mvc·mybatis
九鼎科技-Leo1 天前
什么是 ASP.NET Core?与 ASP.NET MVC 有什么区别?
windows·后端·c#·asp.net·mvc·.net
希忘auto1 天前
详解Java之Spring MVC篇二
java·spring·mvc
LilKevinRay2 天前
【SpringMVC】记录一次Bug——mvc:resources设置静态资源不过滤导致WEB-INF下的资源无法访问
java·笔记·mvc·bug
鹿屿二向箔2 天前
基于 JAVASSM(Java + Spring + Spring MVC + MyBatis)框架开发一个九宫格日志系统
java·spring·mvc
Ares-Wang2 天前
ASP.NET Core 路由规则,自定义特性路由 ,IActionConstraint 路由约束 总结 mvc
后端·asp.net·mvc
老肖相当外语大佬2 天前
解决DDD最大难题-如何划分领域
ddd·领域驱动设计·软件设计
技术拾荒者3 天前
.net core mvc 控制器中页面跳转
后端·c#·asp.net·mvc·.netcore