提升前端代码质量之SOLID设计原则-DIP依赖倒置原则
首页
终于,写到了最后,之前也是更新了SOLID
中的前四种设计原则,不清楚的读者可以移步至【提升前端代码质量之SOLID设计原则系列】第一篇文章 提升前端代码质量之SOLID设计原则-SRP单一职责 本文不再赘述。
SOLID
相关的文章索引:
- 提升前端代码质量之SOLID设计原则-SRP单一职责
- 提升前端代码质量之SOLID设计原则-OCP开放封闭原则
- 提升前端代码质量之SOLID设计原则-LSP里氏替换原则
- 提升前端代码质量之SOLID设计原则-ISP接口隔离原则
- 提升前端代码质量之SOLID设计原则-DIP依赖倒置原则
本文结合代码(React
)主要讲解 SOLID 原则中的DIP - 依赖倒置原则
。
依赖倒置原则(Dependence Inversion Principle)
高层模块不应该依赖低层模块,两个都应该依赖抽象;抽象不应该依赖细节,细节应该依赖抽象
上面的抽象指的是我们定义的方法或者接口,细节指的是我们继承类或者实现接口的方法。
上层调用下层,上层依赖于下层,当下层剧烈变动时上层也要跟着变动,这就会导致模块的复用性降低而且大大提高了开发的成本。假如我们要实现一个商品评论功能,我们可以这么写:
class ModeComment {
startComment() {
console.log('评论...');
}
}
class Mode {
comment: ModeComment;
constructor(comment) {
this.comment = comment;
}
}
const mode = new Mode(new ModeComment())
mode.comment.startComment();
这是没什么毛病的,我们单独把ModeComment
评论功能分开,这也符合单一职责。然后,此时,产品发现如今网购盛行,光评论没什么毛用,需要加一个分享的功能。既然这样,反正分享api实现也不是我写的,我就调下平台api就行。那几秒钟我就给他搞完:
class ModeShare {
shareToWx() {
console.log('分析至微信...');
}
}
class ModeComment {
startComment() {
console.log('评论...');
}
}
class Mode {
comment: ModeComment;
share: ModeShare;
constructor(comment,share) {
this.comment = comment;
this.share = share;
}
}
const mode = new Mode(new ModeComment(),new ModeShare())
mode.comment.startComment();
mode.share.shareToWx();
这样我们就完成了评论+分享功能,可是,这个时候还有点头疼,也不是有bug,而是发现还有可以优化,如果此时产品想要做个折扣的功能,什么?那不是还要改。这个功能简单,全世界都懂,WDNMD(唯独你没懂)。
等等,此时我如果按照上面的模式加上折扣功能,这貌似上层(Mode
类)对下层产生了依赖。这个时候,因为业务的扩展,要从底层实现到高层调用依次地修改代码。
这样一来系统发布过后其实是非常不稳定的。虽然在这个简单的例子中,我们还可以 Hold 住这一次的修改带来的影响,因为都是新增的代码,我们回归的时候也可以很好地 cover
住,但实际我们开发的场景中要复杂得多。
最理想的情况下就是我们编写的代码“永久不变”,那么已经覆盖的单元测试可以不用修改,已经存在的行为就可以保持不变,这就意味着稳定。任何代码上的修改带来的影响都是有未知风险的,不论看上去多么简单。
怎么解决呢,我们可以在上层定义好接口IBaseMode
,然后下层基于IBaseMode
去实现自己的业务,此时依赖便发生了倒置(因为本身上层Mode
类是依赖下层ModeShare
和ModeComment
这两个类,但是此时上层定义好了接口IBaseMode
,下层你只需要按照我定义好的接口实现就好,也就是倒转过来下级依赖于上级定义好的接口),实际过程中,上级只是定义了一些标准,它不会做过多细节的处理,把这个细节交给下层去处理,这样会降低上层与下层的耦合度,提高我们系统搭建的可维护性和稳定性。
我们可以像Vue.use
一样提供一个插件机制,不管如何变化,都能够接收,而不是在内部去保留一份实现。
export interface IBaseMode {
init: (mode) => void;
[key: string]: any;
}
interface IModeInterface{
share?: ModeShare;
comment?: ModeComment;
}
class ModeShare implements IBaseMode {
init(mode) {
mode['share'] = this;
}
shareToWx() {
console.log('分析至微信...');
}
}
class ModeComment implements IBaseMode {
init(mode) {
mode['comment'] = this;
}
startComment() {
console.log('评论...');
}
}
class Mode {
static deps = {};
constructor() {
Object.values(Mode.deps).forEach((item: IBaseMode) => {
item.init(this);
})
}
static inject<T>(dep: T) {
Mode.deps[dep.constructor.name] = dep;
}
}
const share = new ModeShare();
const comment = new ModeComment();
Mode.inject<ModeShare>(share);
Mode.inject<ModeComment>(comment);
const mode: IModeInterface = new Mode();
mode.comment.startComment();
mode.share.shareToWx();
export default Mode;
这时候我们再来看代码,无论我们当前需要加什么需求,对于新的功能,都只需要新建一个类,通过参数传递的方式告诉它,而不需要修改上层的代码。
既然是结合react
项目来讲解的,那我们来看看react
中如何去运用:
//src\page\DIP\LoginForm.tsx
import React from 'react';
import { Button, Form, Input } from 'antd';
import axios from 'axios';
export default () => {
const onFinish = (values: any) => {
axios.post("http://localhost:8000/", values).then(res => {
window.open('https://www.jd.com/')
})
};
return <Form
name="basic"
labelCol={{ span: 8 }}
wrapperCol={{ span: 16 }}
onFinish={onFinish}
autoComplete="off"
>
<Form.Item
label="用户名"
name="username"
rules={[{ required: true, message: '请输入用户名' }]}
>
<Input placeholder='请输入用户名' />
</Form.Item>
<Form.Item
label="密码"
name="password"
rules={[{ required: true, message: '请输入密码' }]}
>
<Input.Password placeholder='请输入密码' />
</Form.Item>
<Form.Item wrapperCol={{ offset: 8, span: 16 }}>
<Button type="primary" htmlType="submit">
提交
</Button>
</Form.Item>
</Form>
}
//src\page\DIP\index.tsx
import React from "react";
import LoginForm from "./LoginForm";
export default () => {
return <>
<LoginForm />
</>
}
上面我们封装了一个登录组件,用户填写完表单过后并且登录成功会跳转到 www.jd.com/ 。此时,如果我们在其他地方也需要复用登录组件提供的ui
,但是登录过后跳转的地址是 www.taobao.com/。 此时我们组件LoginForm
组件内部可以这样实现,通过传入的props
:
//src\page\DIP\LoginForm.tsx
import React from 'react';
import { Button, Form, Input } from 'antd';
import axios from 'axios';
export default (props: { type: '1' | '2' }) => {
const handleAxios1 = (values: any) => {
axios.post("http://localhost:8000/", values).then(res => {
window.open('https://www.jd.com/')
})
}
const handleAxios2 = (values: any) => {
axios.post("http://localhost:8000/", values).then(res => {
window.open('https://www.baidu.com/')
})
}
const onFinish = (values: any) => {
if (props.type === '1') {
handleAxios1(values)
}
if (props.type === '2') {
handleAxios2(values);
}
};
return <Form
name="basic"
labelCol={{ span: 8 }}
wrapperCol={{ span: 16 }}
onFinish={onFinish}
autoComplete="off"
>
...
</Form>
}
//src\page\DIP\index.tsx
import React from "react";
import LoginForm from "./LoginForm";
export default () => {
return <>
<LoginForm type="1"/>
<LoginForm type="2"/>
</>
}
这儿的LoginForm
组件就是我们上层的模块,在使用组件LoginForm
地方我们需要传入不同的type
来区分,根据type
去实现不同的表单提交逻辑。这样一来,只要需要复用LoginForm
组件的地方都需要传不同type
并且在组件内部去修改对应的逻辑。这样一来,显然违背了DIP依赖倒置原则
。此时,可以这样修改:
//src\page\DIP\LoginForm.tsx
import React from 'react';
import { Button, Form, Input } from 'antd';
export default (props: { onSubmit: (value: any) => void }) => {
const { onSubmit } = props;
return <Form
name="basic"
labelCol={{ span: 8 }}
wrapperCol={{ span: 16 }}
onFinish={onSubmit}
autoComplete="off"
>
<Form.Item
label="用户名"
name="username"
rules={[{ required: true, message: '请输入用户名' }]}
>
<Input placeholder='请输入用户名' />
</Form.Item>
<Form.Item
label="密码"
name="password"
rules={[{ required: true, message: '请输入密码' }]}
>
<Input.Password placeholder='请输入密码' />
</Form.Item>
<Form.Item wrapperCol={{ offset: 8, span: 16 }}>
<Button type="primary" htmlType="submit">
提交
</Button>
</Form.Item>
</Form>
}
// src\page\DIP\index.tsx
import React from "react";
import LoginForm from "./LoginForm";
import axios from "axios";
export default () => {
return <>
<LoginForm onSubmit={(values) => {
axios.post("http://localhost:8000/", values).then(res => {
window.open('https://www.jd.com/')
})
}} />
<LoginForm onSubmit={(values) => {
axios.post("http://localhost:8000/", values).then(res => {
window.open('https://www.baidu.com/')
})
}} />
</>
}
这样我们在上层定义好接口,让下层去处理具体的实现。这样一来就大大降低项目的耦合性。
总结
以抽象为基准比以细节为基准搭建起来的架构要稳定得多,所以在拿到需求后,要面向接口编程,先顶层设计再细节地设计代码结构。
相关的代码已经整理成一个仓库上传至github,感兴趣可以结合仓库代码阅读: REACT-SOLID