likes
comments
collection
share

提升前端代码质量之SOLID设计原则-DIP依赖倒置原则

作者站长头像
站长
· 阅读数 12

首页

终于,写到了最后,之前也是更新了SOLID中的前四种设计原则,不清楚的读者可以移步至【提升前端代码质量之SOLID设计原则系列】第一篇文章 提升前端代码质量之SOLID设计原则-SRP单一职责 本文不再赘述。

SOLID相关的文章索引:

本文结合代码(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类是依赖下层ModeShareModeComment这两个类,但是此时上层定义好了接口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