在这篇文章中,我将讨论如何在 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 中完成了。单一职责原则规定的是一个类应该有且仅有一个引起变化的原因,否则应该被拆分。接下来我们看看有多少个理由可以改变我们的组件:
- 我们想要改变请求函数 fetchTodos 来获取待办事项:用其他库比如 axios、api、方法本身等;
- 我们想要添加更多的元素,比如sidebar、menu、footer等等;
- 我们想要修改已存在的元素: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
库。
yarn add axios
然后创建一个 BaseApi
的子类 AxiosApi( api/AxiosApi.ts):
如果我们要将 views/Home.vue
的 BaseApi
(父类)类用 AxiosApi
(子类)替换掉:
这样做已经破坏了我们的应用,因为我们没有遵从里氏原则:“子类继承父类时,除添加新的方法完成新增功能外,尽量不要重写父类的方法。”
如你所见,我们将对象作为参数传递给 AxiosApi
类的 fetch()
方法,但是 BaseApi
类改为使用字符串。在这种情况下,我们不能毫不费力地用父类替换子类。我们只需要做下面的改变即可:
现在我们就可以随意无缝切换 BaseApi
或 AxiosApi
。更深入一些,我们可以创建一个 Api 类来控制子类。
接口隔离原则(ISP)
我们将任务可视为卡片。让我们在 components/TodoRaw.vue 添加一个列表:
然后用列表替换掉卡片:
如你所见,我们在 TodoCard.vue
和 TodoRow.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 现在依赖于高级抽象。