likes
comments
collection
share

服务端模块化架构设计|DDD 领域驱动设计与业务模块化(概念与理解)

作者站长头像
站长
· 阅读数 16

本文为稀土掘金技术社区首发签约文章,14天内禁止转载,14天后未获授权禁止转载,侵权必究!

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

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

RESTful与API设计&管理

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

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

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

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

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

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

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

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

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

未完待续......

在之前的文章 服务端模块化架构设计|项目结构与模块化构建思路 中,我们以掘金的部分功能为例,搭建了一个支持模块化的后端服务项目juejin,其中包含三个模块:juejin-user(用户)juejin-pin(沸点)juejin-message(消息)

通过添加启动模块来任意组合和扩展功能模块

  • 示例1:通过启动模块juejin-appliaction-systemjuejin-user(用户)juejin-message(消息)合并成一个服务减少服务器资源的消耗,通过启动模块juejin-appliaction-pin来单独提供juejin-pin(沸点)模块服务以支持大流量功能模块的精准扩容

  • 示例2:通过启动模块juejin-appliaction-singlejuejin-user(用户)juejin-message(消息)juejin-pin(沸点)直接打包成一个单体应用来运行,适合项目前期体量较小的情况

PS:示例基于IDEA + Spring Cloud

服务端模块化架构设计|DDD 领域驱动设计与业务模块化(概念与理解)

为了能更好的理解本专栏中的模块化,建议读者先阅读 服务端模块化架构设计|项目结构与模块化构建思路

DDD

DDDDomain-Driven Design的缩写,翻译过来叫领域驱动设计,算是一种指导思想吧

我大概是在两年前接触到这玩意儿,网上的文章各种专用名词写的贼高大上,但是我看下来还是云里雾里,于是干脆买了本书来看

服务端模块化架构设计|DDD 领域驱动设计与业务模块化(概念与理解)

这边也建议大家先看书,买纸质的或者找个电子版的都可以,看了书之后会有一个整体的认知,对于各种专有名词的理解更清晰,这样再去看各大厂的实现案例效率才会更高,不然你只能一直在他的专有名词的定义上钻牛角尖

虽然不能说看完之后你就掌握DDD了,毕竟这玩意儿没那么简单,但是如果你能把里面的思想应用到项目中,不需要全部实现,光是能把几个重要的概念落地,你的项目质量就可能提升好几倍,一点不夸张

DDD和我们的现实世界十分相似,很多概念都是生活中的抽象,至少从我的角度来说是这样的,也会在后面的文章有所体现

如何落地

首先,谨记我们使用DDD的目的,比如为了更好的维护性,或是为了梳理复杂的业务使逻辑更清晰

然后,充分的理解每个定义,这样设计或者这种用法是为了解决什么问题

最后,问问自己现在的项目是否存在这些问题,用了DDD之后是否能解决

切忌形式主义,只是模仿示例,最后不但没有解决多少问题反而变得更加复杂,得不偿失

我落地DDD,也没有实现全部内容,而且还经过了一些改造,变得更合适我自己的开发习惯

领域

如何理解领域驱动设计中的领域

DDD中有领域,子域和限界上下文的概念,其作用是为了起到隔离的目的,防止模块之间互相污染,导致耦合度越来越高

假设,我们的juejin-pin(沸点)模块中需要从juejin-user(用户)模块中获得某个用户的信息,我在之前的项目中有看到过这样的偷懒写法

public Map<String, Object> getUser(String id) {
    return userFeign.getUserById(id);
}

然后就在juejin-pin中各种调用,返回的数据一个方法传另一个方法,这个时候负责juejin-user的开发把用户结构改了,正好是你用到的那些字段,你猜猜接下来会发生什么?

你看着已经过了3个月的代码,一边口吐芬芳,一边努力回忆哪些地方用到了这个数据

那如果把Map换成User类呢,不就可以通过重命名字段和方法全量修改了么?

那如果对方把User改成User2了呢?

发现问题了么?你在一味的迁就对方,juejin-pin在一味的迁就juejin-user

所以说做人要有底线,做开发也要有底线,要有边界感,不能一味的迁就

怎么解决呢,那就是设立自己的领域规则。国有国法,家有家规,到了我的地盘,你就要按我的规则来行事

我们都有去银行办卡的经历,需要填写指定的表格或是使用指定的智能终端来录入信息,如果现在不限制,由客户自己提供信息,那就会有用手写的,用打印的,发邮件的,口头表述的,你觉得银行的工作人员会不会爆炸

同样的事,我们在现实生活中自然而然就这样做了,怎么到了开发的时候就净给自己添麻烦呢

所以我们可以定义一个类似PinUser(沸点用户)来表示沸点模块中的用户,在沸点模块中我们只认PinUser,其他的用户一概拒之门外

然后可以添加一个类似于防腐层的接口来做转换,获得用户模块甚至其他系统的用户信息

public class FeignPinUserProvider implements PinUserProvider {

    //省略其他代码...
    
    @Override
    public PinUser getUser(String id) {
        return toPinUser(userFeign.getUserById(id));
    }

    public PinUser toPinUser(Map<String, Object> user) {
        //省略转换过程
    }
}

这样的好处是,不管对方怎么变,我们只需要修改toPinUser这一个方法就可以了,不需要再深入到业务逻辑中到处修改,甚至这个用户不管是掘金用户还是抖音用户和我们的沸点业务都能直接接入

六边形架构

在书中又被称为端口与适配器

以沸点模块为例

服务端模块化架构设计|DDD 领域驱动设计与业务模块化(概念与理解)

整个架构的核心是领域模型与领域服务,也就是所谓的领域驱动

如果我想给前端提供接口,就可以添加一个HTTP适配器

如果我想给前端推送数据,就可以添加一个WS适配器

如果我想持久化业务数据,就可以添加一个MySQL适配器

如果我想要提高查询效率,就可以添加一个Redis适配器

任何功能都可以基于领域服务进行扩展,当我们想要把MySQL换成Oracle时,只需要添加一个Oracle适配器即可,不会对领域中的业务逻辑产生影响(对于Spring的项目来说其实可以等同于持久层框架切换,如MyBatis-Plus换成Jpa

就像我们主机上的显示器接口,让我们可以更换各种各样的显示器而不需要把主机拆开重新烧程序

如果和我们现在用的比较多的MVC架构相比

服务端模块化架构设计|DDD 领域驱动设计与业务模块化(概念与理解)

MVC架构数据模型与存储为核心,业务逻辑只是用来作为更新数据模型的条件,在这种情况下,当我们的数据模型出现问题时(如设计的不合理要重新设计)就会导致整个核心被推翻,连带着服务层,接口层都要根据新的核心重来一遍

六边形架构是以领域模型与领域服务作为核心,如果数据模型出现问题,只要修改或重写一个MySQL适配器或对应的适配器即可

当然六边形架构也会存在核心被推翻的情况,领域模型与领域服务被推翻基本等同于业务被推翻,那么很大可能会是需求的问题而不是架构的问题

领域模型

说到领域模型可能会有实体值对象贫血模型充血模型等概念

和我们平时使用的用于适配数据存储的数据模型的主要区别是

  • 数据模型突出数据,模型本身只是数据的载体,没有业务逻辑

  • 领域模型突出行为,所有的数据变更是通过对应的行为触发

我们平时都是在Service中判断各种各样的逻辑然后调用Set方式设置值,Service就像是你妈(我没有骂人),数据模型就像是你,你妈看了看炸鸡说吃了不好,然后给你Set了一把青菜,你妈看了看天气今天降温了,然后给你Set了一条秋裤

但是我们正常的生活肯定不是这样,不然你妈不得累死,她只要和你说把衣服穿好起床吃饭,你就会自己穿衣服然后自己动手吃饭,所以Service应该只需要调用一个方法,然后告诉你饭菜在桌上,衣服在衣柜里,把这些参数传给你,你就可以自己动手把事情做好

以沸点模型为例,假设我们将对沸点的评论对评论的回复对回复的回复都当作一种模型,用一个字段来区分是对沸点还是评论还是回复

数据模型

//沸点模型
{
    "id": "沸点ID",
    "content": "沸点内容",
    "clubId": "沸点圈子ID",
    "userId": "沸点用户ID",
    "createTime": "沸点发布时间"
}

//评论模型
{
    "id": "评论ID",
    "comment": "评论内容",
    "userId": "评论用户ID",
    "pinId": "沸点ID",
    "commentId": "评论ID,存在则为回复,不存在则为评论",
    "createTime": "评论时间"
}

我们一般情况下会基于关系型数据库来设计表结构以及对应的数据模型,在Service中编写逻辑设置值

领域模型

//沸点模型
{
    "id": "沸点ID",
    "content": "沸点内容",
    "club": {
        "id": "沸点圈子ID",
        "name": "沸点圈子名称",
        "logo": "沸点圈子图标",
        "description": "沸点圈子描述"
    },
    "user": {
        "id": "沸点用户ID",
        "name": "沸点用户名称",
        "profilePicture": "沸点用户头像"
    },
    comments: [{
        "id": "评论ID",
        "comment": "评论内容",
        "user": {
            "id": "评论用户ID",
            "name": "评论用户名称",
            "profilePicture": "评论用户头像"
        },
        comments: [{
            "id": "回复ID",
            "comment": "回复内容",
            "user": {
                "id": "回复用户ID",
                "name": "回复用户名称",
                "profilePicture": "回复用户头像"
            },
            comments: [],
            likes: [],
            "createTime": "回复时间"
        }],
        likes: [],
        "createTime": "评论时间"
    }],
    likes: [],
    "createTime": "沸点发布时间"
}

通过领域模型的行为更新数据(伪代码)

//评论
Pin pin = getPin(id);
pin.addComment(Comment comment);

//回复评论
Pin pin = getPin(id);
Comment comment = pin.getComment(commentId);
comment.addComment(Comment comment);

//回复回复
Pin pin = getPin(id);
Comment comment = pin.getComment(commentId);
Comment reply = comment.getComment(replyId);
reply.addComment(Comment comment);

我们通过让领域模型做某件事来实现业务的逻辑,简单的说就是自己的事情自己做

领域模型更接近我们对于现实世界的理解,强化了模型与模型之间的逻辑结构和特定行为

总结

在我的理解中,领域模型是更接近现实描述的一种可视化模型,是更能让产品,设计,测试理解的模型

因为领域模型更接近需求,同时又因为所有人都是在同一个现实世界中,对于事物的认知在同一层面上,以至于每个人对于同一个事物的模型理解会趋近相同,也就是领域模型唯一性(可能在命名上会有区别,但是在整个层次结构上应该都是一致的)

所以一旦一个领域模型确定之后,只要需求不发生大的改变,领域模型就基本不需要修改,这样的话,出现由于前期设计问题从而导致需求不满足等一些风险的概率应该是比我们现在的开发模式要小的,我觉得这就是DDD最大的优势

源码

上一篇:网关路由模块化支持与条件配置

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