思考:领域驱动设计(DDD)对前端有何助力

4,786 阅读6分钟

DDD - Domain-driven design,领域驱动设计。很多人喜欢把事情分的很清,这是后端的设计思想,对我们前端有什么影响呢?其实不然,javascript是一种面向对象的开发语言,既然是面向对象,我们就可以适用任何的设计思想。

当我们在讨论DDD的时候,我们在讨论什么?

什么是Domain?如何driven design?这些太过拘于概念,我们以一些实际的例子,来看看,DDD是一种什么思想。

以个人为例,我们拥有年龄、身高、体重这样的属性,先排出其他影响因素,我们假设身高和体重只受到年龄影响,由此我们可以进行如下设计,在class Person中,包含三个Property分别是age年龄、weight体重,height身高。

通常来说,在年龄的增长过程中,体重、身高会有不同阶段的变化,我们假设变化的公式为weight = f(age)height = f(age)则有下面序列图

也就是weightheight的变化是受age影响的,那么如果我们统一封装在Person类中,这个类的领域就会变得很奇怪,所谓基本的属性,为什么类本身会需要对基本的属性有方法去处理变化呢?

领域驱动设计在这里引入了 值对象 的概念,根据 单一职责迪米特法则,把weightheight这样的具有变化特点的值,转为对象的形式,也就是面试对象三大特点之一的 封装,所谓 封装变化

// Weight.js
class Weight {
  #weight = "opps";

  constructor(age) {
    this.#weight = this._handleAgeStateWeight(age);
  }

  getWeight() {
    return this.#weight;
  }

  _handleAgeStateWeight(age) {
    if (age < 24) return "normal weight";
    else return "fat weight";
  }
}

// Height.js
class Height {
  #height = "opps";

  constructor(age) {
    this.#height = this._handleAgeStateHeight(age);
  }

  getHeight() {
    return this.#height;
  }

  _handleAgeStateHeight(age) {
    if (age < 24) return "normal height";
    else return "no growing";
  }
}

// Person.js
class Person {
  #age = 24;
  #weight = "opps";
  #height = "opps";

  constructor(age = 24) {
    this.#age = age;
    this.#weight = new Weight(this.#age).getWeight();
    this.#height = new Height(this.#age).getHeight();
  }

  getPersonInfo() {
    console.log("age", this.#age);
    console.log("weight", this.#weight);
    console.log("height", this.#height);
  }
}

看到这里,你们是否对DDD领域驱动设计有一定了解了呢?

个人拙见:值对象的概念是 单一职责迪米特法则 的合集,更注重颗粒度,无疑,这种做法实现了面向对象的高内聚低耦合,但是同样的,在内存开销和项目初期增加了巨大的问题。

领域驱动设计是一种大型项目构建比较推荐的设计,通过这样的设计,我们可以对项目形成较好对管理成本,接手项目的时候,可以对最小颗粒进行版本迭代和更新从而产生最小的影响。

这不是必须要求你的项目就一定要这样做

因为初期成本太高,不适合初创型公司以这样的形式去设计项目。

在前端里,领域驱动设计又意味着什么呢

很多时候,作为一个前端开发,我们会以页面作为最小颗粒,也就是在设计的时候,我们会认为一个页面就是一个 实体(实体可以理解为值对象的集合),一个menu的模块,就是 聚合(聚合可以理解为实体的集合)。在理解上面这两个名词之后,我们从最小颗粒(页面)开始看起。

页面通常包含 N 个组件,在React或者Vue中,我们使用Ant Design或者ElementUi,这里我仅用React举例。

一个React的页面Container,是一些Component的合集,我们把页面的变化进行了封装,因为我们认为业务的变化会产生页面的变化。这一点没错,但是,相反的,我们去详细的说应该叫 业务的变化会引起部分页面中组件的变化,我们一概而论的把变化封装在了更高一层的页面上,实际上就是没有进行良好的组件拆分。

那么我们继续解耦,我们将每一个Component作为一个值对象去看,这样在业务变化的时候,我们就可以进一步的去修改特定的组件。

那么,领域驱动设计到此为止了吗?

在我看来,这是开始。

从第二节看下来,通篇的文字都是业务的变化,这就是最大的问题。我们所有的设计,都是基于业务。这并不符合DDD,因为DDD是领域驱动设计,也就是在DDD看来,设计的根本是因为领域。那么领域是如何去定义,我们就需要进行探讨了。

我们还是看一个比较常见的场景。

在上面这个需求里,我们可以看到有SelectTable构成的一个Component,如果我们将此Component作为值对象的话,如果我Select区域要新增一些条件,或者Table需要有改动,进行的修改,都是需要在此Component上,所以,在DDD中,我认为,构成的最小颗粒应该是由 UI 库提供的组件是值对象,而我们的Component级别的封装应该是 实体 ,构成的页面应该是 聚合

// RecentSales.jsx
import Header from "./component/Header";
import Content from "./component/Content";

export default () => {
  return [
    <Header key="recentSalesHeader" />,
    <Content key="recentSalesContent" />
  ];
};
// ./component/Header/index.jsx
import Title from "./entity/Title";
import SelectTime from "./entity/SelectTime";

export default () => {
  return [<Title key="headerTitle" />, <SelectTime key="headerSelectTime" />];
};
// ./component/Header/entity/Title/index.jsx
import { Typography } from "antd";

const { Title, Text } = Typography;

export default () => {
  return [<Title key="titleTitle" />, <Text key="titleText" />];
};
// ./component/Header/entity/SelectTime/index.jsx
import { DatePicker } from "antd";

export default () => {
  return <DatePicker />;
};
// ./component/Content/index.jsx
import SalesTable from "./entity/SalesTable";

export default () => {
  return <SalesTable />;
};
// ./component/Content/entity/SalesTable/index.jsx
import { Table } from "antd";

export default () => {
  return <Table />;
};

讲到这里,我希望你有所收获,无论是你对DDD领域驱动设计的不屑一顾,还是你从中有了新的见解。

之后,我要开始讲一些 奇怪 的东西了。

刚刚我们不断的在说,DDD是在进行封装最小变化,那么问题来了,<Table />这样的组件,具有可扩展性吗?所有的可拓展性,都是基于样式层面的调整,如果我想对表格的渲染过程有不同的设计呢?如果我对表格的渲染需要惰性分页,滚动分页呢?你会发现,并不支持,所以,为什么中台系统是ant design这类 UI 库主打,是因为通用性极强,此类库提供了“最佳”实践,特殊的需求要自己写。看起来没什么问题,但是在我看来,不具备阔拓展性。

所以我希望的,或者我所设想的组件库,应该充分利用extends,达到如下的效果:

也就是我们不在自己构建一个全新的值对象,而是在通用的基础上,去继承并极大的增加复用性。

以上都是个人的一些见解啦,DDD是一个还在摸索阶段的东西,来吧,关注 ihap 技术黑洞,我是 ihap肥少。