Skip to content

设计模式

设计模式(Design pattern)是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结。使用设计模式是为了可重用代码、让代码更容易被他人理解、保证代码的可靠性。

设计模式大抵可分为创建型结构型行为型

image.png

创建型

工厂模式

工厂模式根据抽象程度可分为三种,分别为简单工厂、工厂方法和抽象工厂。其核心在于将创建对象的过程封装其他,然后通过同一个接口创建新的对象。简单工厂模式又叫静态工厂方法,用来创建某一种产品对象的实例,用来创建单一对象。

javascript
// 简单工厂
class Factory {
  constructor(username, pwd, role) {
    this.username = username
    this.pwd = pwd
    this.role = role
  }
}
class CreateRoleFactory {
  static create(username, pwd, role) {
    return new Factory(username, pwd, role)
  }
}
const admin = CreateRoleFactory.create('张三', '222', 'admin')

在实际工作中,各用户角色所具备的能力是不同的,因此简单工厂是无法满足的,这时候就可以考虑使用工厂方法来代替。工厂方法的本意是将实际创建对象的工作推迟到子类中。

javascript
class User {
  constructor(name, menuAuth) {
    if (new.target === User) throw new Error('User 不能被实例化')
    this.name = name
    this.menuAuth = menuAuth
  }
}
class UserFactory extends User {
  constructor(...props) {
    super(...props)
  }
  static create(role) {
    const roleCollection = new Map([
      ['admin', () => new UserFactory('管理员', ['首页', '个人中心'])],
      ['user', () => new UserFactory('普通用户', ['首页'])],
    ])

    return roleCollection.get(role)()
  }
}
const admin = UserFactory.create('admin')
console.log(admin) // {name: "管理员", menuAuth: Array(2)}
const user = UserFactory.create('user')
console.log(user) // {name: "普通用户", menuAuth: Array(1)}

随着业务形态的变化,一个用户可能在多个平台上同时存在,显然工厂方法也不再满足了,这时候就要用到抽象工厂。抽象工厂模式是对类的工厂抽象用来创建产品类簇,不负责创建某一类产品的实例。

javascript
class User {
  constructor(hospital) {
    if (new.target === User) throw new Error('抽象类不能实例化!')
    this.hospital = hospital
  }
}
// 浙一
class ZheYiUser extends User {
  constructor(name, departmentsAuth) {
    super('zheyi_hospital')
    this.name = name
    this.departmentsAuth = departmentsAuth
  }
}
// 萧山医院
class XiaoShanUser extends User {
  constructor(name, departmentsAuth) {
    super('xiaoshan_hospital')
    this.name = name
    this.departmentsAuth = departmentsAuth
  }
}
const getAbstractUserFactory = (hospital) => {
  switch (hospital) {
    case 'zheyi_hospital':
      return ZheYiUser
      break
    case 'xiaoshan_hospital':
      return XiaoShanUser
      break
  }
}
const ZheYiUserClass = getAbstractUserFactory('zheyi_hospital')
const XiaoShanUserClass = getAbstractUserFactory('xiaoshan_hospital')
const user1 = new ZheYiUserClass('王医生', ['外科', '骨科', '神经外科'])
console.log(user1)
const user2 = newXiaoShanUserClass('王医生', ['外科', '骨科'])
console.log(user2)

小结: 构造函数和创建对象分离,符合开放封闭原则。 使用场景: 比如根据权限生成不同用户。 将 new 操作简单封装,遇到 new 的时候就应该考虑是否用工厂模式;

单例模式

单例模式理解起来比较简单,就是保证一个类只能存在一个实例,并提供一个访问它的全局接口。
单例模式又分懒汉式和饿汉式两种,其区别在于懒汉式在调用的时候创建实例,而饿汉式则是在初始化就创建好实例,具体实现如下:

javascript
// 懒汉式
class Single {
  static getInstance() {
    if (!Single.instance) {
      Single.instance = new Single()
    }
    return Single.instance
  }
}
const test1 = Single.getInstance()
const test2 = Single.getInstance()
console.log(test1 === test2) // true
javascript
// 饿汉式
class Single {
  static instance = new Single()
  static getInstance() {
    return Single.instance
  }
}
const test1 = Single.getInstance()
const test2 = Single.getInstance()
console.log(test1 === test2) // true

小结: 实例如果存在,直接返回已创建的,符合开放封闭原则。 使用场景: Redux、Vuex 等状态管理工具,还有我们常用的 window 对象、全局缓存等。

原型模式

对于前端来说,原型模式在常见不过了。当新创建的对象和已有对象存在较大共性时,可以通过对象的复制来达到创建新的对象,这就是原型模式。

javascript
// Object.create()实现原型模式
const user = {
  name: 'zhangsan',
  age: 18,
}
let userOne = Object.create(user)
console.log(userOne.__proto__) // {name: "zhangsan", age: 18}
// 原型链继承实现原型模式
class User {
  constructor(name) {
    this.name = name
  }
  getName() {
    return this.name
  }
}
class Admin extends User {
  constructor(name) {
    super(name)
  }
  setName(_name) {
    return (this.name = _name)
  }
}
const admin = new Admin('zhangsan')
console.log(admin.getName())
console.log(admin.setName('lisi'))

小结: 原型模式最简单的实现方式---Object.create()。 使用场景: 新创建对象和已有对象无较大差别时,可以使用原型模式来减少创建新对象的成本。

结构型

装饰器模式

讲装饰器模式之前,先聊聊高阶函数。高阶函数就是一个函数就可以接收另一个函数作为参数。

javascript
const add = (x, y, f) => {
  return f(x) + f(y)
}
const num = add(2, -2, Math.abs)
console.log(num) // 4

函数 add 就是一个简单的高阶函数,而 add 相对于 Math.abs 来说相当于一个装饰器,因此这个例子也可以理解为一个简单的装饰器模式。
react 中,高阶组件 (HOC) 也是装饰器模式的一种体现,通常用来不改变原来组件的情况下添加一些属性,达到组件复用的功能。

javascript
import React from 'react'
const BgHOC = (WrappedComponent) =>
  class extends React.Component {
    render() {
      return (
        <div style={{ background: 'blue' }}>
          <WrappedComponent />
        </div>
      )
    }
  }

小结: 装饰器模式将现有对象和装饰器进行分离,两者独立存在,符合开放封闭原则和单一职责模式。 使用场景: es7 装饰器、vue mixins、core-decorators 等。

适配器模式

适配器别名包装器,其作用是解决两个软件实体间的接口不兼容的问题。以 axios 源码为例:

javascript
function getDefaultAdapter() {
  var adapter;
  // 判断当前是否是 node 环境
  if (typeof process !== 'undefined' && Object.prototype.toString.call(process) === '[object process]') {
    // 如果是 node 环境,调用 node 专属的 http 适配器
    adapter = require('./adapters/http');
  } else if (typeof XMLHttpRequest !== 'undefined') {
    // 如果是浏览器环境,调用基于 xhr 的适配器
    adapter = require('./adapters/xhr');
  }
  return adapter;
}
// http adapter
module.exports = function httpAdapter(config) {
  return new Promise(function dispatchHttpRequest(resolvePromise, rejectPromise) {
    ...
  }
}
// xhr adapter
module.exports = function xhrAdapter(config) {
  return new Promise(function dispatchXhrRequest(resolve, reject) {
    ...
  }
}

其目的就是保证 node 和浏览器环境的入参 config 一致,出参 Promise 都是同一个。 小结: 不改变原有接口的情况下,统一接口、统一入参、统一出参、统一规则,符合开发封闭原则。 使用场景 :拥抱变化,兼容代码。

代理模式

代理模式就是为对象提供一个代理,用来控制对这个对象的访问。
在我们业务开发中最常见的有四种代理类型:事件代理,虚拟代理、缓存代理和保护代理。
虚拟代理,其最具代表性的例子就是图片预加载。预加载主要是为了避免网络延迟、或者图片太大引起页面长时间留白的问题。通常的解决方案是先给 img 标签展示一个占位图,然后创建一个 Image 实例,让这个实例的 src 指向真实的目标图片地址,当其真实图片加载完成之后,再将 DOM 上的 img 标签的 src 属性指向真实图片地址。

javascript
class ProxyImg {
  constructor(imgELe) {
    this.imgELe = imgELe
    this.DEFAULT_URL = 'xxx'
  }
  setUrl(targetUrl) {
    this.imgEle.src = this.DEFAULT_URL
    const image = new Image()

    image.onload = () => {
      this.imgEle.src = targetUrl
    }
    image.src = targetUrl
  }
}

缓存代理常用于一些计算量较大的场景。当计算的值已经被出现过的时候,不需要进行第二次重复计算。以传参求和为例:

javascript
const countSum = (...arg) => {
  console.log('count...')
  let result = 0
  arg.forEach((v) => (result += v))
  return result
}
const proxyCountSum = (() => {
  const cache = {}
  return (...arg) => {
    const args = arg.join(',')
    if (args in cache) return cache[args]
    return (cache[args] = countSum(...arg))
  }
})()
proxyCountSum(1, 2, 3, 4) // count...  10
proxyCountSum(1, 2, 3, 4) // 10

小结: 通过修改代理类来增加功能,符合开放封闭模式。 使用场景: 图片预加载、缓存服务器、处理跨域以及拦截器等。

行为型

策略模式

介绍策略模式之前,简单实现一个常见的促销活动规则:

  • 预售活动,全场 9.5 折
  • 大促活动,全场 9 折
  • 返场优惠,全场 8.5 折
  • 限时优惠,全场 8 折

人人喊打的 if-else

javascript
const activity = (type, price) => {
  if (type === 'pre') {
    return price * 0.95
  } else if (type === 'onSale') {
    return price * 0.9
  } else if (type === 'back') {
    return price * 0.85
  } else if (type === 'limit') {
    return price * 0.8
  }
}

以上代码存在肉眼可见的问题:大量 if-else、可扩展性差、违背开放封闭原则等。我们再使用策略模式优化:

javascript
const activity = new Map([
  ['pre', (price) => price * 0.95],
  ['onSale', (price) => price * 0.9],
  ['back', (price) => price * 0.85],
  ['limit', (price) => price * 0.8],
])
const getActivityPrice = (type, price) => activity.get(type)(price)
// 新增新手活动
activity.set('newcomer', (price) => price * 0.7)

小结: 定义一系列算法,将其一一封装起来,并且使它们可相互替换。符合开放封闭原则。 使用场景: 表单验证、存在大量 if-else 场景、各种重构等。

观察者模式

观察者模式又叫发布-订阅模式,其用来定义对象之间的一对多依赖关系,以便当一个对象更改状态时,将通知其所有依赖关系。通过“别名”可以知道,观察者模式具备两个角色,即“发布者”和“订阅者”。正如我们工作中的产品经理就是一个“发布者”,而前后端、测试可以理解为“订阅者”。以产品经理建需求沟通群为例:

javascript
// 定义发布者类
class Publisher {
  constructor() {
    this.observers = []
    this.prdState = null
  }
  // 增加订阅者
  add(observer) {
    this.observers.push(observer)
  }
  // 通知所有订阅者
  notify() {
    this.observers.forEach((observer) => {
      observer.update(this)
    })
  }
  // 该方法用于获取当前的 prdState
  getState() {
    return this.prdState
  }
  // 该方法用于改变 prdState 的值
  setState(state) {
    // prd 的值发生改变
    this.prdState = state
    // 需求文档变更,立刻通知所有开发者
    this.notify()
  }
}
// 定义订阅者类
class Observer {
  constructor() {
    this.prdState = {}
  }
  update(publisher) {
    // 更新需求文档
    this.prdState = publisher.getState()
    // 调用工作函数
    this.work()
  }
  // work 方法,一个专门搬砖的方法
  work() {
    // 获取需求文档
    const prd = this.prdState
    console.log(prd)
  }
}
// 创建订阅者:前端开发小王
const wang = new Observer()
// 创建订阅者:后端开发小张
const zhang = new Observer()
// 创建发布者:产品经理小曾
const zeng = new Publisher()
// 需求文档
const prd = {
  url: 'xxxxxxx',
}
// 小曾开始拉人入群
zeng.add(wang)
zeng.add(zhang)
// 小曾发布需求文档并通知所有人
zeng.setState(prd)

经常使用 Event Bus(Vue)Event Emitter(node)会发现,发布-订阅模式和观察者模式还是存在着细微差别,即所有事件的发布/订阅都不能由发布者和订阅者“私下联系”,需要委托事件中心处理。以 Vue Event Bus 为例:

javascript
import Vue from 'vue'
const EventBus = new Vue()
Vue.prototype.$bus = EventBus
// 订阅事件
this.$bus.$on('testEvent', func)
// 发布/触发事件
this.$bus.$emit('testEvent', params)

整个过程都是 this.$bus 这个“事件中心”在处理。

  • 小结: 为解耦而生,为事件而生,符合开放封闭原则。
  • 使用场景: 跨层级通信、事件绑定等。

迭代器模式

迭代器模式号称“遍历专家”,它提供一种方法顺序访问一个聚合对象中的各个元素,且不暴露该对象的内部表示。迭代器又分内部迭代器(jquery.each/for...of)和外部迭代器(es6 yield)。在 es6 之前,直接通过 forEach 遍历 DOM NodeList 和函数的 arguments 对象,都会直接报错,其原因都是因为他们都是类数组对象。对此 jquery 很好的兼容了这一点。在 es6 中,它约定只要数据类型具备 Symbol.iterator 属性,就可以被 for...of 循环和迭代器的 next 方法遍历。

javascript
;(function (a, b, c) {
  const arg = arguments
  const iterator = arg[Symbol.iterator]()

  console.log(iterator.next()) // {value: 1, done: false}
  console.log(iterator.next()) // {value: 2, done: false}
  console.log(iterator.next()) // {value: 3, done: false}
  console.log(iterator.next()) // {value: undefined, done: true}
})(1, 2, 3)

通过 es6 内置生成器 Generator 实现迭代器并没什么难度,这里重点通 es5 实现迭代器:

javascript
function iteratorGenerator (list) {
  var index = 0;
  // len 记录传入集合的长度
  var len = list.length;
  return {
    // 自定义 next 方法
    next: funciton () {
      // 如果索引还没有超出集合长度,done 为 false
      var done = index >= len;
      // 如果 done 为 false,则可以继续取值
      var value = !done ? list[index++] : undefined;
      // 将当前值与遍历是否完毕(done)返回
      return {
        done: done,
        value: value
      };
    }
  }
}
var iterator = iteratorGenerator([1, 2, 3]);
console.log(iterator.next()); // {value: 1, done: false}
console.log(iterator.next()); // {value: 2, done: false}
console.log(iterator.next()); // {value: 3, done: false}
console.log(iterator.next()); // {value: undefined, done: true}
  • 小结: 实现统一遍历接口,符合单一功能和开放封闭原则。
  • 使用场景: 有遍历的地方就有迭代器。

君子慎独