# 前端设计模式

# javascript 设计模式

主要结合实际例子说明以下三种模式:

  • 策略模式
  • 发布 - 订阅模式
  • 装饰器模式
  • 适配器模式
  • 代理模式
  • 责任链模式

# 设计模式概念

官方解释一点就是 : 模式是一种可服用的解决方案,用于解决软件设计中遇到的常见问题。

我举的这个例子,你应该知道,什么是设计模式了吧?引用修言 (opens new window)老哥的一句话 : 烹饪有菜谱,游戏有攻略,干啥都有一些能够让我们达到目标的 “套路”,在程序世界,编程的“套路” 就是设计模式。

设计模式就是在软件设计、开发过程中,针对特定问题、场景的更优解决方案。

# 题目一

我们先来做一个题,很简单的,大家肯定都做过 权限逻辑 判断吧?

需求 : 只用当用户满足以下条件,才能看阿宽的这篇文章

给大家 3min,代码怎么写? “ 呵,你这不是看不起老夫吗?老夫拿起键盘,就是 if-else 梭哈,直接带走,下一个 ! ”

function checkAuth(data) {
  if (data.role !== 'juejin') {
    console.log('不是掘金用户')
    return false
  }
  if (data.grade < 1) {
    console.log('掘金等级小于 1 级')
    return false
  }
  if (data.job !== 'FE') {
    console.log('不是前端开发')
    return false
  }
  if (data.type !== 'eat melons') {
    console.log('不是吃瓜群众')
    return false
  }
}

这段代码的问题在哪?

  • checkAuth 函数会爆炸 💥
  • 策略项无法复用
  • 违反开闭原则

聪明的小伙伴已经知道这里要讲的是什么模式了,对头!这里讲的就是 策略模式。那么什么是策略模式呢 ?

# 策略模式

定义 : 要实现某一个功能,有多种方案可以选择。我们定义策略,把它们一个个封装起来,并且使它们可以相互转换。

策略 + 组合,绝配啊,老哥!

我们用策略模式来改造以下这段逻辑 👇

// 维护权限列表
const jobList = ['FE', 'BE']

// 策略
var strategies = {
  checkRole: function(value) {
    if (value === 'juejin') {
      return true
    }
    return false
  },
  checkGrade: function(value) {
    if (value >= 1) {
      return true
    }
    return false
  },
  checkJob: function(value) {
    if (jobList.indexOf(value) > 1) {
      return true
    }
    return false
  },
  checkEatType: function(value) {
    if (value === 'eat melons') {
      return true
    }
    return false
  },
}

我们已经写完了策略,接下来要做的就是验证了~

// 校验规则
var Validator = function() {
  this.cache = []

  // 添加策略事件
  this.add = function(value, method) {
    this.cache.push(function() {
      return strategies[method](value)
    })
  }

  // 检查
  this.check = function() {
    for (let i = 0; i < this.cache.length; i++) {
      let valiFn = this.cache[i]
      var data = valiFn() // 开始检查
      if (!data) {
        return false
      }
    }
    return true
  }
}

此时,小彭同学需要进行权限验证的条件为 :

  • 掘金用户
  • 掘金等级 1 级以上

那么代码就可以这么写 :

// 小彭使用策略模式进行操作
var compose1 = function() {
  var validator = new Validator()
  const data1 = {
    role: 'juejin',
    grade: 3,
  }
  validator.add(data1.role, 'checkRole')
  validator.add(data1.grade, 'checkGrade')
  const result = validator.check()
  return result
}

然后另一个小伙伴阿宽,他可能需要进行权限验证的条件为 :

  • 掘金用户
  • 前端工程师

那么代码就可以这么写 :

// 阿宽使用策略模式进行操作
var compose2 = function() {
  var validator = new Validator()
  const data2 = {
    role: 'juejin',
    job: 'FE',
  }
  validator.add(data2.role, 'checkRole')
  validator.add(data2.job, 'checkJob')
  const result = validator.check()
  return result
}

这是不是比一直疯狂写 if-else 好太多了呢?还有什么例子?表单验证啊 ~ 对于表单字段(名称、密码、邮箱、....)我们可以使用策略模式去设计优化它,想啥呢,赶紧动手试一下!我都已经手把手教你到这了~

# 什么时候用策略模式?

当你负责的模块,基本满足以下情况时

  • 各判断条件下的策略相互独立且可复用
  • 策略内部逻辑相对复杂
  • 策略需要灵活组合

# 题目二

前面还逼逼一下,这里直接给需求了 👇

需求 : 申请成功后,需要触发对应的订单、消息、审核模块对应逻辑

机智如我,我会如何做呢?

function applySuccess() {
  // 通知消息中心获取最新内容
  MessageCenter.fetch()
  // 更新订单信息
  Order.update()
  // 通知相关方审核
  Checker.alert()
}

不就这样写吗,还想咋滴!!!是的,这么写没得毛病,但是呢,我们来思考几个问题

比如 MessageCenter.fetch() 是小彭写的,他大姨夫来了,心情不爽,把模块的方法名改了,现在叫 MessageCenter.request(),你咋办,你这块逻辑改呗~

再比如,你和阿宽并行开发的,阿宽负责订单模块,你一气呵成写下这段代码,然后一运行,报错了,一询问,发现,原来阿宽昨晚去蹦迪了,原本今天应该完成的订单模块Order.update(),延迟一天,那你就只能先注释代码,等依赖的模块开发完了,你再回来添加这段逻辑咯~

更可怕的是,你可能不只是涉及到这三个模块,maybe 还有很多模块,比如你申请成功,现在还需要上报申请日志,你总不能这样写吧?

function applySuccess() {
  // 通知消息中心获取最新内容
  MessageCenter.fetch();
  // 更新订单信息
  Order.update();
  // 通知相关方审核
  Checker.alert();

  // maybe 更多
  Log.write();
  ...
}

到这里,我们的 发布-订阅模式 要按捺不住了。

# 发布 - 订阅模式

啊哈哈哈,有没有觉得这个EventEmitter好熟悉啊,这不是面试常会问的?

发布 - 订阅是一种消息范式,消息的发布者,不会将消息直接发送给特定的订阅者,而是通过消息通道广播出去,然后呢,订阅者通过订阅获取到想要的消息。

我们用 发布 - 订阅模式 修改以下上边的代码 👇

const EventEmit = function() {
  this.events = {}
  this.on = function(name, cb) {
    if (this.events[name]) {
      this.events[name].push(cb)
    } else {
      this.events[name] = [cb]
    }
  }
  this.trigger = function(name, ...arg) {
    if (this.events[name]) {
      this.events[name].forEach((eventListener) => {
        eventListener(...arg)
      })
    }
  }
}

上边我们写好了一个 EventEmit,然后我们的业务代码可以改成这样 ~

let event = new EventEmit();
event.trigger('success');

MessageCenter.fetch() {
  event.on('success', () => {
    console.log('更新消息中心');
  });
}
Order.update() {
  event.on('success', () => {
    console.log('更新订单信息');
  });
}
Checker.alert() {
  event.on('success', () => {
    console.log('通知管理员');
  });
}

但是这样就没问题了吗?其实还是有弊端的,比如说,过多的使用发布订阅,就会导致难以维护调用关系。所以,还是看大家的设计吧,这里只是让大家知道,发布订阅模式是个啥~

# 什么时候用发布 - 订阅模式?

当你负责的模块,基本满足以下情况时

  • 各模块相互独立
  • 存在一对多的依赖关系
  • 依赖模块不稳定、依赖关系不稳定
  • 各模块由不同的人员、团队开发

我知道你有疑问,关于 观察者模式 VS 发布 - 订阅模式,这里我不讲它们的区分,下期再聊,或者自行资料查询

# 题目三

这个题目,也有点难想啊,我直接说吧,主要讲 装饰器模式适配器模式

# 装饰器模式

个人理解 : 是为了给一个函数赋能,增强它的某种能力,它能动态的添加对象的行为,也就是我传入的就是一个对象

在 JS 世界中,世间万物,皆为对象

大家过年,都会买桔子树吧(不买的统一带走),意味 “大吉大利” 嘛,那么我们买了桔子树之后,都会往上边挂一些红包,摇身一变,“红包桔子树”,牛掰!这个的红包就是装饰器,它不对桔子树原有的功能产生影响。

再举个 🌰,我现在写的这边文章,我只会写中文,但是各位看官中有英国小伙伴,那我不会写英文啊,所以我需要通过装饰器来赋予我写英文的能力

你这不是在真实写代码中的啊,能不能举一个日常开发的 🌰,ok,那我来举一个,React 中的高阶组件 HOC

了解 React 的都知道,高阶组件其实就是一个函数,接收一个组件作为参数,然后返回一个新的组件。

那么我们现在写一个高阶组件 HOC,用它来装饰 Target Component

import React from 'react'

const yellowHOC = (WrapperComponent) => {
  return class extends React.Component {
    render() {
      ;<div style={{ backgroundColor: 'yellow' }}>
        <WrapperComponent {...this.props} />
      </div>
    }
  }
}

export default yellowHOC

定义了一个带有装饰黄色背景的高阶组件,我们用它来装饰目标组件

import React from 'react'
import yellowHOC from './yellowHOC'

class TargetComponent extends Reac.Compoment {
  render() {
    return <div>66666</div>
  }
}

export default yellowHOC(TargetComponent)

你看,我们这不就用到了装饰器模式了嘛?什么,你还听不懂?那我最后再举一个例子,不知道这个例子,能不能帮助你们理解

const kuanWrite = function() {
  this.writeChinese = function() {
    console.log('我只会写中文')
  }
}

// 通过装饰器给阿宽加上写英文的能力
const Decorator = function(old) {
  this.oldWrite = old.writeChinese
  this.writeEnglish = function() {
    console.log('给阿宽赋予写英文的能力')
  }
  this.newWrite = function() {
    this.oldWrite()
    this.writeEnglish()
  }
}

const oldKuanWrite = new kuanWrite()
const decorator = new Decorator(oldKuanWrite)
decorator.newWrite()

# 适配器模式

个人理解,为了解决我们不兼容的问题,把一个类的接口换成我们想要的接口。

举个 🌰,我想听歌的时候,我发现我没带耳机,我的手机是 iphone 的,而现在我只有一个 Type-C 的耳机,为了能够听歌,我用了一个转换器(也就是适配器),然后我就可以开心的听歌了。

再以上边的为例子,小彭来看阿宽写的文章,他只会看英文的,突然阿宽用日文来写,他看不懂,所以他需要通过适配器模块,去帮他转成日文,从而满足他的需求

/** ---------------新的代码------------ */
const kuanLanguage = function() {
  this.japaneseLanguage = function() {
    console.log('阿宽写日文')
  }
}

const pengLookLanguage = function() {
  this.lookLanguage = function() {
    console.log('小彭只看得懂英文')
  }
}

// 通过适配器进行接口适配,从而满足我的需求
const AdapterLanguage = function(user) {
  this.language = user.lookLanguage
}

AdapterLanguage.prototype = new kuanLanguage()
AdapterLanguage.prototype.japaneseLanguage = function(name) {
  console.log(`通过适配器, ${name}也能听日文了`)
}

const peng = new pengLookLanguage()
const adapterPeng = new AdapterLanguage(peng)
adapterPeng.japaneseLanguage('小彭')

# 题目四

我们再来讲一个叫做 代理模式,说到代理哈,我脑海里第一个浮现的词语 : “事件委托、事件代理”,这算吗?算哒。我举些 🌰,让大家知道代理模式是个啥玩意

作为程序员嘛,女朋友比较难找,就算找到了,咱这么瘦弱,怕是保护不了啊,所以我花钱找了个保镖来保护我,稳妥。这就是代理模式。

你翻墙吗?你能 google 吗?老实人哪会什么翻墙,我是不会的,会我也说我不会。其实正常来讲,我们直接访问 google 是无响应的。那怎么办呢,通过第三方代理服务器。VyprVPN ? ExpressVPN ?懂 ?

要说初中非主流三大巨头,莫过于 许嵩、徐良、汪苏泷了,去年想去看许嵩演唱会,好家伙,这个演唱会的门票都被抢光了,无奈之下,只能找黄牛,这里,黄牛就起了代理的作用,懂?

程序世界的代理者也是如此,我们不直接操作原有对象,而是委托代理者去进行。代理者的作用,就是对我们的请求预先进行处理或转接给实际对象。

代理模式是为其它对象提供一种代理以控制这个对象的访问,具体执行的功能还是这个对象本身,就比如说,我们发邮件,通过代理模式,那么代理者可以控制,决定发还是不发,但具体发的执行功能,是外部对象所决定,而不是代理者决定。

// 发邮件,不是qq邮箱的拦截
const emailList = ['qq.com', '163.com', 'gmail.com']

// 代理
const ProxyEmail = function(email) {
  if (emailList.includes(email)) {
    // 屏蔽处理
  } else {
    // 转发,进行发邮件
    SendEmail.call(this, email)
  }
}

const SendEmail = function(email) {
  // 发送邮件
}

// 外部调用代理
ProxyEmail('cvte.com')
ProxyEmail('ojbk.com')

下边再来举一个例子,来至 《JavaScript 设计模式与开发实践》

// 本体
var domImage = (function() {
  var imgEle = document.createElement('img')
  document.body.appendChild(imgEle)
  return {
    setSrc: function(src) {
      imgEle.src = src
    },
  }
})()

// 代理
var proxyImage = (function() {
  var img = new Image()
  img.onload = function() {
    domImage.setSrc(this.src) // 图片加载完设置真实图片src
  }
  return {
    setSrc: function(src) {
      domImage.setSrc('./loading.gif') // 预先设置图片src为loading图
      img.src = src
    },
  }
})()

// 外部调用
proxyImage.setSrc('./product.png')

# 什么时候用代理模式?

当你负责的模块,基本满足以下情况时

  • 模块职责单一且可复用
  • 两个模块间的交互需要一定限制关系

这里我又知道你有疑问了,关于 代理模式 VS 装饰者模式,这里我不讲它们的区分,下期再聊,或者自行资料查询

# 题目五

需求 :如图所示,我们申请设备之后,接下来要选择收货地址,然后选择责任人,而且必须是上一个成功,才能执行下一个~

小伙伴们惊讶了,这不简单嘛?奥力给!

function applyDevice(data) {
  // 处理巴拉巴拉...
  let devices = {}
  let nextData = Object.assign({}, data, devices)
  // 执行选择收货地址
  selectAddress(nextData)
}

function selectAddress(data) {
  // 处理巴拉巴拉...
  let address = {}
  let nextData = Object.assign({}, data, address)
  // 执行选择责任人
  selectChecker(nextData)
}

function selectChecker(data) {
  // 处理巴拉巴拉...
  let checker = {}
  let nextData = Object.assign({}, data, checker)
  // 还有更多
}

你看,这不就完事了,有啥难的,然后过了第二天,你又接了两个新的流程需求,可能一个就两步骤,一个可能多了 “检查库存” 这个步骤

你不由惊了,哎呀妈呀,老夫聊发少年狂,键盘伺候,Ctrl C + Ctrl V,直接 copy 然后改一下逻辑??

这里就是要讲的责任链模式。

# 责任链模式

什么是责任链模式呢?我给你们找了个定义 : 避免请求发送者与接收者耦合在一起,让多个对象都有可能接收请求,将这些对象连接成一条链,并且沿着这条链传递请求,直到有对象处理它为止。

const Chain = function(fn) {
  this.fn = fn

  this.setNext = function() {}

  this.run = function() {}
}

const applyDevice = function() {}
const chainApplyDevice = new Chain(applyDevice)

const selectAddress = function() {}
const chainSelectAddress = new Chain(selectAddress)

const selectChecker = function() {}
const chainSelectChecker = new Chain(selectChecker)

// 运用责任链模式实现上边功能
chainApplyDevice.setNext(chainSelectAddress).setNext(chainSelectChecker)
chainApplyDevice.run()

这样的好处是啥?首先是解耦了各节点关系,之前的方式是 A 里边要写 B,B 里边写 C,但是这里不同了,你可以在 B 里边啥都不写。

其次,各节点灵活拆分重组,正如上边你接的两个新需求。比如两个步骤的你就只需要这么写完事

const applyLincense = function() {}
const chainApplyLincense = new Chain(applyLincense)

const selectChecker = function() {}
const chainSelectChecker = new Chain(selectChecker)

// 运用责任链模式实现上边功能
chainApplyLincense.setNext(chainSelectChecker)
chainApplyLincense.run()

# 什么时候使用责任链模式?

当你负责的模块,基本满足以下情况时

  • 你负责的是一个完整流程,或你只负责流程中的某个环节
  • 各环节可复用
  • 各环节有一定的执行顺序
  • 各环节可重组

# 结尾

不知不觉,又是给大家撸了一篇设计模式的文章,设计模式真的很重要,虽然说我也刚领悟其中的一丝诀窍,但是我想,可能还有很多跟我一样,在设计模式门槛外,迟迟无法顿悟的小伙伴,我是一个很烦看长篇大论的人,之前也看了设计模式相关书籍,初次看还能耐心看下去,但是去看别人博客、文章的时候,看到好多定义、好多理论,包括一些举的 🌰,感觉当时懂了,但是在开发中,我还是不知道怎么用...

不是让大家强行套用设计模式,而是想表达 : 我们首先需要理解,其次需要形成一种肌肉记忆,正如前边说的策略模式、发布 - 订阅模式的例子一样,大家在真实开发场景中肯定都有遇到,只是没有想到,原来这就是设计模式,或者说,原来这里可以用到设计模式去设计。

这篇文章,其中三个题目来自潇哥分享的 PPT,并且思维导图也是他画的,经过他的同意,我将其整理成了一篇文章,同时结合我自己的理解,尽可能以一种幽默、风趣、易懂的形式,跟大家唠嗑唠嗑一下,如果我有说的不对或者存在误解的地方,欢迎大家指出~ Thank you ~

对了,前几天写了一篇 前端渣渣的我再也不敢说我会写 Button 组件了 (opens new window) 文章,然后发现被人转到个人公众号了,要不是学弟告知我,我还不知道原来我的文章被公众号营销给转走了。虽然备注了原文出处,但是还是想说,你转我的文章,最起码评论区里知会一声吧~

看过我文章的小伙伴都知道,我几乎是一个月才更一两篇博客,不是我不想更新,而是我想做有生命有活力的文章,是真的有去了解、去做实践、去看源码,然后一字一字的在电脑桌前,整理成一篇算得上能有一丝帮助的文章,写这么一篇,可能就需要我一下午甚至一天的时间,你倒好,直接 Ctrl C + Ctrl V,把我文章抄过去了,然后再贴个原文链接,我...

我不是不允许你转,你最起码告知我一声吖~ 我 : ???????

算了算了,不说了,感谢看官看到这里,文中源码我放这里了 设计模式 demo 源码 (opens new window),想看的自取 ~

# 相关链接

更新于: 4/27/2020, 12:31:02 PM