浅谈DDD(domain-driven design-领域驱动设计)

原文地址: https://juejin.im/post/6845166891670093838

划了半年,现在开始接客!

❝本篇文章存在大量干货,建议调整姿势反复观看,所有技术栈通用,本文以vue项目为例❞

「好代码一定是设计出来的!而不是用多么牛逼的技术栈」

DDD

注意这不是大笑表情包,DDD(domain-driven design-领域驱动设计),大部分前端接到需求的时候都在思考这个原型我要怎么实现某块功能细节(用哪个UI库、该怎么写),即使不了解业务也一样可以开发,通常也能完成工作,这种情况称为 ——「面向功能编程(没有思考的前端资源)」

然而,随着业务的深入、需求不断地迭代和变更,我们(前端)仿佛在负重前行,为什么?

  • 业务不熟悉,不知道为什么要改需求,也不知道修改需求影响了原来哪块代码
  • 历史包袱多,投入相较后端越来越重

领域怎么划分?

[data:image/svg+xml;utf8,<?xml version="1.0"?><svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="1030" height="284"></svg>](data:image/svg+xml;utf8,<?xml version="1.0"?><svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="1030" height="284"></svg>)

差不多就是这样抽象(就硬划),建议和后端同学进行 「深入♂♂探讨」(抽象能力个人认为需要学习思考+后天培养,也不能指望文章)

❝我认为应该在了解产品(或行业领域)的前提下进行软件开发,先根据项目抽象出业务边界模块,建立领域再动手开发。❞

沙箱

[data:image/svg+xml;utf8,<?xml version="1.0"?><svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="570" height="374"></svg>](data:image/svg+xml;utf8,<?xml version="1.0"?><svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="570" height="374"></svg>)

❝哎呀,你这个代码怎么参数乱七八糟,方法全依赖在一起我都不敢改啊,怎么一个js有9000行❞

我们先来康康什么是沙箱(sandbox),安全独立的,隔离外界的,互不影响的环境结构。

看起来好像没什么,我们再看看设计模式应该准许你的准则,

  • 单一原责
  • 迪米特(最少知识)原则
  • 开放封闭原则
  • 依赖倒置
  • 接口隔离
  • 里式替换

是不是觉得DDD和设计原则、沙箱环境都有相同想强调的地方?

好像又绕回来了,唠了半天,尽和我搞文绉绉的文字游戏,是你需要30万还是想爬山????了?

不是这样的,如果

「「希望删除和新增一块代码(领域),并不影响原有代码,并且不需要改动也不会报错,怎么做?」」

接下来教大家如何运用在你的前端工程中。

结构

普通工程的目录结构

[data:image/svg+xml;utf8,<?xml version="1.0"?><svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="552" height="490"></svg>](data:image/svg+xml;utf8,<?xml version="1.0"?><svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="552" height="490"></svg>)

很传统的目录结构,包括vue-cli3也是这么做的,按照功能职责划分,想修改路由前往routes文件夹里修改,修改vuex去store里找。

[data:image/svg+xml;utf8,<?xml version="1.0"?><svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="500" height="500"></svg>](data:image/svg+xml;utf8,<?xml version="1.0"?><svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="500" height="500"></svg>)

按DDD划分的目录结构(非战斗人员请迅速撤离)

ps: 这里的领域文件夹名不大对,当作错误示范将就看了

[data:image/svg+xml;utf8,<?xml version="1.0"?><svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="588" height="658"></svg>](data:image/svg+xml;utf8,<?xml version="1.0"?><svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="588" height="658"></svg>)

贴一份新目录结构的说明

[data:image/svg+xml;utf8,<?xml version="1.0"?><svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="876" height="816"></svg>](data:image/svg+xml;utf8,<?xml version="1.0"?><svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="876" height="816"></svg>)

优点

为什么要这样做?

职责

假设一个项目多人开发,拆分需求一定是按照某块业务功能划分的(分配工作),这就是我们在无意识中利用DDD的思想拆分产品业务。

  • 清晰定位了每个同事的工作职责和边界
  • 出现问题方便定位到问题的范围
  • 减少代码的冲突

效能

  • 维护成本低
  • 可复用某块业务领域
  • 可拓展(在统一的模式下做自动化、基建等)

缺点

  • 代码风格和编程思路的转换
  • 老项目的兼容 (在历史债上转换)

领域内职责&细节

❝因害怕篇幅过长(懒),所以本文使用了大量伪代码(意识流装逼)❞

数据访问对象 DAO (Data Access Object)

 static async getXxxList(api, payload) {
    const res = await api(payload);
    const { results, totalCount } = res.data.data;
    return { data: results, total: totalCount };
  }
复制代码

没什么好说的,处理领域内的接口,api 接口都放在这里,如果要处理数据建议也在这里完成,做到视图与数据分离,不要在view里再做数据的封装。

如果处理非常复杂建议再分层DTO(Data Transfer Object),把逻辑放在这里面。

❝当你发现有接口重复的时候建议复写,一般不会超过3个如果超过了思考是否领域拆分有问题❞

参数/枚举 Model

里面一般有俩个文件夹:

枚举 Enums

一些selecet的内容

[
  ["发布成功", 1],
  ["未发布", 2],
  ["等待中", 3],
  ["发布失败", 4]
  ....
]
复制代码

对应表对象 Vo

放一些对应表结构的数据,包括需要默认的内容、参数

{
  // 团队 Number  teamId = null;

  // 应用 String  appName = null;

  // 申请人 Enums(Role)  role = 1;

  // 开始时间 dateStr  beginTime = null;

  // 结束时间 dateStr  endTime = null;
}
复制代码

路由 Router

注意,通常我们都是写在最外层一个统一的router文件夹来注册汇总。

「但是在领域里需要自己的路由,根据领域的实现和业务逻辑来规划细分路由。」

视图 View

类似路由的理念,可以在同一领域下拆分为多个视图,毕竟有些庞大的业务需要很多页面才能支撑起来他的流转。

中台系统的某个领域内可能需要2-3套crud才能支撑起来,那其中的View文件夹下可能就要放2-3套对应的crud文件夹,文件夹内部才是视图的结构(搜索表单、表格、详情等按需细分)。

Vuex

正常用法,领域内不同单元之间需要流通数据可用,不建议跨领域使用

领域注册 Main.js

[data:image/svg+xml;utf8,<?xml version="1.0"?><svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="1280" height="641"></svg>](data:image/svg+xml;utf8,<?xml version="1.0"?><svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="1280" height="641"></svg>)

这里考虑到每个领域需要赋予的能力都不一样,所以需要针对单个领域模块提供可选松耦合的能力。

有点发布订阅模式内味了,注册了什么东东?先上一段神奇的代码

import routes from "./router";
export { routes };

// 领域模块名称const MODULE_NAME = "Doctor";

// 注册模块能力export default ({
  registerRouter,
  registerStore,
  registerApi,
  .......
}) => {
 // 使用模块能力  registerRouter(MODULE_NAME, routes);
  registerStore(MODULE_NAME, store);
};

复制代码

看到了很多register开头的参数这些是自定义的能力,从能力中心拿出注册。

「能力分析」

能力的订(相当于注册能力的名单封装),在最后会用到这里

  • registerRouter 把路由注册到Doctor模块下
 Doctor: {
    path: "/doctor/",
    name: "doctor",
    redirect: "/doctor/goodDoctor",
    component: () => import(/* webpackChunkName: "goodDoctor" */ "../view/Main"),
    children: [
      {
        path: "/doctor/goodDoctor",
        name: "goodDoctor",
        component: () =>
          import(/* webpackChunkName: "goodDoctor" */ "../view/goodDoctor/Index")
      },
      {
        path: "/doctor/badDoctor",
        name: "badDoctor",
        component: () =>
          import(/* webpackChunkName: "badDoctor" */ "../view/badDoctor/Index")
      },
    ]
  }
复制代码

  • registerStore 把doctor的store注册到vuex里
 Doctor: {
    goodDoctor:{
        namespaced: true,
        state,
        getters,
        mutations,
        actions
    },
    badDoctor:{
        namespaced: true,
        state,
        getters,
        mutations,
        actions
    }
 }
复制代码

还有很多就不细说了

「扩展」

  • 针对单个领域的埋点
  • 针对单个领域的性能监控
  • 针对单个领域的错误监控

还有很多遐想(瞎想)空间......

中心化-领域汇总 Module.js

问:整了那么多领域,怎么让他实例化把项目跑起来?

答:将领域汇总挂载到vue上

// 获取src下的领域import 领域1 from "@/领域1/main";
import 领域2 from "@/领域2/main";
——————————————————————————————————————
/*
也可以用
require.context('@', false, /\\main.js/)
匹配所有src下领域内的main文件注册
*/

const modules = [领域1,领域2,领域3....];
new Center(modules).mount("#app");
复制代码

这个中心函数Center到底做了什么了什么?

import Vue from "vue";
import App from "../App";

class Center {
  constructor(modules) {
  
    // modulesCenter做了层遍历,注册的所有内容放进函数以便获取    modulesCenter(modules);

    Center.store = this.store = modulesCenter.createStore();
    Center.router = this.router = modulesCenter.createRouter();
    Center.vue = this.vue = new Vue({
      store: this.store,
      router: this.router,
      render: h => h(App)
    });
  }

  mount(mountEl) {
    this.vue.$mount(mountEl);
    return (Center.currentCenter = this);
  }
}

export default Center;
复制代码

大致做了什么,估计你们也猜到了

  • createRouter
 const router = new VueRouter({
    mode: "history",
    ...modules.router
 });
复制代码

  • createStore
 const store = new Vuex.Store({
    ...modules.store
 });
复制代码

共享领域数据

说白了就是跨领域调用,其实都在本地了,代码和调用vuex modules是一毛一样的

computed: {
    //都在 Doctor 内    ...mapAction('Doctor',[
      '/goodDoctor',
      '/badDoctor',
    ])
}
复制代码

将来的composition Api 可能会更加轻哦。

❝当然是不建议有耦合数据的,如果出现跨领域了依赖,说明拆分有问题,反推后端这里是否可以进行改变,防患于未然,面向未来的需求提前编程。❞

基本能力讲完了是不是觉得这么做的意义和消耗的时间不成正比? 让我们把能利再升华一下,看看如何扩展成架构反哺业务......

[data:image/svg+xml;utf8,<?xml version="1.0"?><svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="400" height="400"></svg>](data:image/svg+xml;utf8,<?xml version="1.0"?><svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="400" height="400"></svg>)

扩展基建中台

  • 代码统一规范
  • 领域代码复用(也能跨项目)
  • 提升效能,结合webpack5+vite可以只coding个别领域
  • 搭建微前端体系前的技术筹备统一

领域gui化

https://user-gold-cdn.xitu.io/2020/7/2/1730fb84f701ebd5?imageView2/0/w/1280/h/960/format/webp/ignore-error/1

使用某个领域

直接拖拽,low code或者微服务都可以,不限于输出形式

https://user-gold-cdn.xitu.io/2020/7/2/1730fb4213072566?imageView2/0/w/1280/h/960/format/webp/ignore-error/1

配套设施

需要管理后台进行收集与输出,其它配套设施一大堆就不过多描述了

总结(人话时间)

  1. 思考业务
  2. 可插拔
  3. 提效
上一篇:关于Spring事务<tx:annotation-driven/>的理解


下一篇:springmvc中mvc:annotation-driven的说明