如何避免在Vue应用中违反SOLID原则

2022-06-16 16:33:50 浏览数 (1)

在这篇文章中,我将讨论如何在 Vue 应用中使用 SOLID 原则。

SOLID 包括以下观点:

  • 单一职责原则
  • 开闭原则
  • 里氏替换原则
  • 依赖倒置原则
  • 接口隔离原则

接下来我们看看如何在 Vue 实战中避免这些原则,我们从一个 TODO LIST 项目中去体会这些观点。

准备

用 vue cli 初始化一个 Vue 项目。

代码语言:javascript复制
vue create todo-app

我们用 vue2.6.10 typescript3.4.3 构建我们的应用。如果你对 typescript 不熟悉,可以查看typescriptlang。

安装完成之后,将目录中的组件都删除掉,然后我们的 src 目录如下图所示:

App.vue:

views/Home.vue:

准备工作就绪,接下来正式进入正题。

单一职责原则(SRP)

首先我们将 views/Home.vue 组件改成如下代码,通过API获取一个任务列表并展示出来:

基本上所有的功能我们都在 views/Home.vue 中完成了。单一职责原则规定的是一个类应该有且仅有一个引起变化的原因,否则应该被拆分。接下来我们看看有多少个理由可以改变我们的组件:

  1. 我们想要改变请求函数 fetchTodos 来获取待办事项:用其他库比如 axios、api、方法本身等;
  2. 我们想要添加更多的元素,比如sidebar、menu、footer等等;
  3. 我们想要修改已存在的元素:header 或者 todo list;

如上面所述,我们轻易地找到3条理由去修改这个 views/Home.vue。 当这个应用的功能越来越丰富时,真正的问题将开始:代码越来越多直到我们都不知道自己写了什么(这就意味着该组件失去了控制)。

通过将上述可能存在的变动提取到不同的函数、类或者组件中,我们就可以避免违反单一职责原则。接下来进行重构:

第一步,将我们的请求函数放到新的API文件中(新建 srcapiapi.ts):

第二步,我们将 header 组件提取成一个新的函数组件 components/Header.vue:

最后一步,我们提取 TODO LIST 组件 components/TodoList.vue:

然后修改 views/Home.vue:

重构之后,可以看到我们的 Home 组件更加的单一和可读。

开闭原则(OCP)

让我们把目光聚焦在 components/TodoList.vue。这个组件展示了一系列待做任务卡片。如果需求变更,我们要改变这些卡片或者将它们用表格的形式展示,会发生什么?答案是我们必须修改这个组件(它看起来非常死板)。开闭原则规定“当应用的需求改变时,在不修改软件实体的源代码或者二进制代码的前提下,可以扩展模块的功能,使其满足新的需求。”现在我们来重构 TodoList 组件,达到避免这种窘境!

我们通过插槽来改变这个情况:

然后将我们的卡片独立成一个组件 components/TodoCard.vue:

最后更新我们的 Home 组件:

这样一来,我们就能够轻易地通过其它组件来修改任务显示。

里氏替换原则(LSP)

这节聚焦在 API 部分。首先我们将 api.ts 重新命名并将它放到一个独立的文件 api/BaseApi.ts:

如你所见, BaseApi 类有一个 fetch 方法需要一个参数 url。因为一些原因,我们决定使用 axios 库。

代码语言:javascript复制
yarn add axios

然后创建一个 BaseApi 的子类 AxiosApi( api/AxiosApi.ts):

如果我们要将 views/Home.vueBaseApi(父类)类用 AxiosApi(子类)替换掉:

这样做已经破坏了我们的应用,因为我们没有遵从里氏原则:“子类继承父类时,除添加新的方法完成新增功能外,尽量不要重写父类的方法。

如你所见,我们将对象作为参数传递给 AxiosApi 类的 fetch()方法,但是 BaseApi 类改为使用字符串。在这种情况下,我们不能毫不费力地用父类替换子类。我们只需要做下面的改变即可:

现在我们就可以随意无缝切换 BaseApiAxiosApi。更深入一些,我们可以创建一个 Api 类来控制子类。

接口隔离原则(ISP)

我们将任务可视为卡片。让我们在 components/TodoRaw.vue 添加一个列表:

然后用列表替换掉卡片:

如你所见,我们在 TodoCard.vueTodoRow.vue 中将整个 todo 对象作为 prop 发送,但是我们仅使用此对象的一部分。 userId 在两个组件中都没用到, id 仅在 TodoCard.vue 中使用。我们这就违反了接口隔离原则“组件不应该依赖没有使用到的属性和方法”。

有两种方式可以解决这个问题:

  • 将 TODO 接口缩减成更小的接口
  • 仅仅传输使用到的属性给组件

使用函数式组件来重构可视化组件, views/Home.vue:

components/TodoCard.vue:

components/TodoRow.vue:

依赖倒置原则(DIP)

依赖倒置原则的原始定义为:高层模块不应该依赖低层模块,两者都应该依赖其抽象;抽象不应该依赖细节,细节应该依赖抽象。

高层模块和低层模块是什么意思?

  • 低层模块实现最基本的操作,例如API;
  • 高层模块包含复杂的业务逻辑,该逻辑指导低层陌路爱类执行某项操作。

我们在 types 中为 Api 类创建一个新的接口:

接着更新我们所有的 api 类和 views/Home.vue: 更新 api/api.ts:

api/AxiosApi.ts:

api/BaseApi.ts:

views/Home.vue:

现在,低级类 Api 和高级 views/Home.vue 类依赖同一个接口 IApi。

原始依赖关系的方向已经颠倒了:低级 Api 现在依赖于高级抽象。

0 人点赞