游离在设计模式中的开放封闭原则: 软件实体应该是可扩展,而不可修改的。也就是说,对扩展是开放的,而对修改是封闭的。 因此,开放封闭原则主要体现在两个方面:

  • 对扩展开放,意味着有新的需求或变化时,可以对现有代码进行扩展,以适应新的情况。
  • 对修改封闭,意味着类一旦设计完成,就可以独立完成其工作,而不要对类进行任何修改。

# 1. 单例模式

一个类仅有一个实例

// 静态属性形式
class Person {
  show() {
    console.log('我是单例模式')
  }

  static setInstance() {
    if (!Person.instance) {
      Person.instance = new Person()
    }
    return Person.instance
  }
}

// 闭包模式
Person.setInstance = (function() {
    let instance = null
    return function() {
      if (!instance) {
        instance = new Person()
      }
      return instance
    }
})()

# Vuex中的单例模式

与上面的setInstance相似

let Vue // 这个Vue的作用和楼上的instance作用一样
...
export function install (_Vue) {
  // 判断传入的Vue实例对象是否已经被install过Vuex插件(是否有了唯一的state)
  if (Vue && _Vue === Vue) {
    if (process.env.NODE_ENV !== 'production') {
      console.error(
        '[vuex] already installed. Vue.use(Vuex) should be called only once.'
      )
    }
    return
  }
  // 若没有,则为这个Vue实例对象install一个唯一的Vuex
  Vue = _Vue
  // 将Vuex的初始化逻辑写进Vue的钩子函数里
  applyMixin(Vue)
}

单例模式保证每一个Vue实例只会被install一次Vuex插件,所以每个 Vue 实例只会拥有一个全局的 Store。如果没有单例模式会造成数据丢失,每次都会在当前vue实例注入新的store

# 原型模式

原型模式不仅是一种设计模式,它还是一种编程范式,是js面向对象系统实现的根基。

# 装饰器模式

不改变原对象的基础上,通过对其进行包装拓展,使原有对象可以满足用户的更复杂需求。

es5实现

const Modal = (function() {
    let modal = null
    return function() {
        if (!modal) {
            modal = document.createElment('div')
            modal.innerHTML = '您还未登录哦'
            modal.id = 'modal'
            modal.style.display = 'none'
            document.body.appendChild(modal)
        }
        return modal
    }
})()

document.getElementById('open').addEventListener('click', function() {
    openModal()
    changeButtonStatus()
})

document.getElementById('close').addEventListener('click', function() {
    let modal = new Modal()
    modal.style.display = 'none'
})
// 将展示Modal的逻辑单独封装
function openModal() {
    const modal = new Modal()
    modal.style.display = 'block'
}
// 新功能
function newFunction1 () {
      const btn = document.getElementById('open')
      btn.innerText = '快去登录'
}
function newFunction2 () {
    const btn =  document.getElementById('open')
    btn.setAttribute("disabled", true)
}
// 新版本功能逻辑整合
function changeButtonStatus() {
    changeButtonText()
    disableButton()
}

es6实现

// 定义打开按钮
class OpenButton {
    onClick() {
        const modal = new Modal()
        modal.style.display = 'block'
    }
}

// 定义按钮对应的装饰器
class Decorator {
    constructor(open_button) {
            this.open_button = open_button
        }
    }
    onClick() {
        this.open_button.onClick()
        // "包装"了一层新逻辑
        this.changeButtonStatus()
    }
    changeButtonStatus() {
        this.changeButtonText()
        this.disableButton()
    }
    disableButton() {
        const btn =  document.getElementById('open')
        btn.setAttribute("disabled", true)
    }
    changeButtonText() {
        const btn = document.getElementById('open')
        btn.innerText = '快去登录'
    }
}
const openButton = new OpenButton()
const decorator = new Decorator(openButton)

document.getElementById('open').addEventListener('click', function() {
    // openButton.onClick()
    // 此处可以分别尝试两个实例的onClick方法,验证装饰器是否生效
    decorator.onClick()
})

es7实现


# 策略模式

将臃肿的if-else模式进行业务拆分,提取逻辑,分发优化 比如将下面逻辑

逻辑分析:现有sm,mid,max三种获取n值方式,通过不同的tag获取不同的值

  • 0 - 5 之间返回原值n
  • 5 - 10 之间返回原值n - 1
  • 10 - 100 之间返回原值 n - 2
function transfer(tag, n) {
  if (tag === 'min') {
    if (n > 0 & n <= 5) {
      return n
    }
  }
  if (tag === 'mid') {
    if (n > 5 & n <= 10) {
      return n - 1
    }
  }
  if (tag === 'max') {
    if (n > 10 & n <= 100) {
      return n - 2
    }
  }
}

将内部各个函数体进行提取,用map管理逻辑体

const numberProcessor = {
  sm(n) {
    if (n > 0 & n <= 5) {
      return n
    }
  },
  mid(n) {
    if (n > 5 & n <= 10) {
      return n - 1
    }
  }
  max(n) {
    if (n > 10 & n <= 100) {
      return n - 1
    }
  }
}

const numberDone = (tag, n) => {
  return numberProcessor[tag](n)
}

# 状态模式

状态模式和策略模式很像,解决的问题没啥本质的差别,它们都封装行为、都通过委托来实现行为分发

策略模式中的行为函数是”潇洒“的行为函数,它们不依赖调用主体、互相平行、各自为政,井水不犯河水。

状态模式中的行为函数,首先是和状态主体之间存在着关联,由状态主体把它们串在一起;另一方面,正因为关联着同样的一个(或一类)主体,所以不同状态对应的行为函数可能并不会特别割裂。即**允许一个对象在其内部状态改变时改变它的行为,对象看起来似乎修改了它的类。**

逻辑分析:四种咖啡的咖啡机体内,蕴含着四种状态:

  • 美式咖啡态(american):只吐黑咖啡
  • 普通拿铁态(latte):黑咖啡加点奶
  • 香草拿铁态(vanillaLatte):黑咖啡加点奶再加香草糖浆
  • 摩卡咖啡态(mocha):黑咖啡加点奶再加点巧克力
class CoffeeMaker {
  constructor() {
    this.state = 'init' // 初始化咖啡机状态
  }

  changeState(state) {
    this.state = state
    if (state === 'american') {
      console.log('制作黑咖啡')
    } else if (state === 'latte') {
      console.log('黑咖啡加点奶')
    } else if (state === 'vanillaLatte') {
      console.log('黑咖啡加点奶再加点香草糖浆')
    } else if (state === 'mocha') {
      console.log('黑咖啡加点奶再加点巧克力')
    }
  }
}

const mk = new CoffeeMaker();
mk.changeState('latte'); // 输出 '黑咖啡加点奶'

通过状态模式实现,状态模式需要对函数主体状态有感知(需要随时都可以拿到函数主体中的各个属性)

class CoffeeMaker {
  constructor() {
    this.state = 'init' // 初始化咖啡模式
    this.leftMilk = '500ml' // 初始化牛奶存储量
  }
  stateProcceror = {
    that: this, // 拿到主体,后续可以任何时候获取主体的属性
    american() {
      // 尝试在行为函数里拿到咖啡机实例的信息并输出
      console.log('咖啡机现在的牛奶存储量是:', this.that.leftMilk)
      console.log('制作黑咖啡')
    },
    latte() {
      this.american()
      console.log('加点奶')
    },
    vanillaLatte() {
      this.latte();
      console.log('再加香草糖浆')
    },
    mocha() {
      this.latte();
      console.log('再加巧克力')
    }
  }
  changeState(state) {
    this.state = state
    const fn = this.stateProcceror[state]
    if (!fn) return
    fn()
  }
}

const mk = new CoffeeMaker()
mk.changeState('latte')

# 观察者模式 - 发布订阅模式

观察者模式定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个目标对象,当这个目标对象的状态发生变化时,会通知所有观察者对象,使它们能够自动更新。生活场景中比如公司群里领导发消息,领导(发布者)一发消息,所有人需要关注领导发言的(订阅者)都能收到推送信息,这就是观察者模式。

逻辑分析:

  • 发布者:铲平经理拉群(增加订阅者),然后是@所有人(通知订阅者)。此外作为群主&产品经理,还具有踢走项目组成员(移除订阅者)的能力
  • 订阅者:监听发布者的消息更新通知
// 定义发布者
class Publisher {
  constructor() {
    this.observers = []
    console.log('publisher created')
  }
  add(observer) {
    console.log('publisher add invoked')
    this.observers.push(observer)
  }
  remove(observer) {
    console.log('publisher remove invoked')
    this.observers = this.observers.filter(item => item !== observer)
  }
  notify() {
    this.observers.forEach(observer => {
      observer.update(this)
    })
  }
}
// 定义订阅者 - 被通知、 去执行
class Observer {
  constructor() {
    console.log('Observer created')
  }
  update() {
    console.log('Observer update invoked')
  }
}

以上,我们就完成了最基本的发布者和订阅者类的设计和编写。在实际的业务开发中,我们所有的定制化的发布者/订阅者逻辑都可以基于这两个基本类来改写。比如我们可以通过拓展发布者类,来使所有的订阅者来监听某个特定状态的变化

// 定义一个具体的需求文档(prd)发布类
class PrdPublisher extends Publisher {
  constructor() {
    super()
    // 初始化需求文档
    this.prdState = null
    // 产品经理还没有拉群,开发群目前为空
    this.observers = []
    console.log('PrdPublisher created')
  }
  // 获取当前的prdState
  getState() {
    console.log('PrdPublisher getState invoked')
    return this.prdState
  }
  // 改变prdState
  setState(state) {
    console.log('PrdPublisher setState invoked')
    this.prdState = state
    // 需求变更立即通知所有订阅者
    this.notify()
  }
}

// 作为订阅方,开发者的任务也变得具体起来:接收需求文档、并开始干活
class DeveloperObserver extends Observer {
  constructor() {
    super()
    // 需求文档一开始还不在,prd为空
    this.prdState = {}
    console.log('DeveloperObserver created')
  }
  update(publisher) {
    console.log('DeveloperObserver update invoked')
    // 更新需求文档
    this.prdState = publisher.getState()
    // 调用工作函数
    this.word()
  }
  // 工作函数
  work() {
    // 获取需求文档
    const prd = this.prdState
    console.log('996 begins...o(╥﹏╥)o')
  }
}

下面我们来看看韩梅梅和她的小伙伴们是如何搞事情的吧:

// 创建订阅者:前端开发李雷
const liLei = new DeveloperObserver()
// 创建订阅者:服务端开发小A
const A = new DeveloperObserver()
// 创建订阅者:测试同学小B
const B = new DeveloperObserver()
// 韩梅梅出现了
const hanMeiMei = new PrdPublisher()
// 需求文档出现了
const prd = {
    // 具体的需求内容
    ...
}
// 韩梅梅开始拉群
hanMeiMei.add(liLei)
hanMeiMei.add(A)
hanMeiMei.add(B)
// 韩梅梅发送了需求文档,并@了所有人
hanMeiMei.setState(prd)

# 命令模式

接收者、命令者、发布者

class Receiver { // 接收者类
  execute() {
    console.log('接收者执行请求)
    // ... 一些请求
  }
}

class Command { // 命令对象
  constructor(receiver) {
    this.receiver = receiver
  }
  execute() { // 调用接收者对应接口执行
    console.log('命令对象-》接收者-》对应接口执行')
    this.receiver.execute()
  }
}

class Invoker { // 发布者
  constructor(command) {
    this.command = command
  }
  invoke() { // 发布请求,调用命令对象
    console.log('发布者发布请求')
    this.command.execute()
  }
}

const warehouse = new Receiver() // 厨师
const order = new Command(warehouse) // 订单
const client = new Invoker(order) // 请求者
client.invoke()

# 例子 - 菜单

一个界面,包含很多个按钮,每个按钮有不同的功能,我们利用命令模式来完成

<button id="button1"></button>
<button id="button2"></button>
<button id="button3"></button>

<script>
  var button1 = document.getElementById("button1");
  var button2 = document.getElementById("button2");
  var button3 = document.getElementById("button3");
</script>

然后定义一个setCommand函数,负责将按钮安装命令,可以确定的是,点击按钮会执行某个 command 命令,执行命令的动作被约定为调用 command 对象的 execute() 方法。如下:

var button1 = document.getElementById('button1')
var setCommand = function(button, conmmand) {
  button.onclick = function() {
    conmmand.execute()
  }
}

点击按钮之后具体行为包括刷新菜单界面、增加子菜单和删除子菜单等,这几个功能被分布在 MenuBar 和 SubMenu 这两个对象中:

var MenuBar = {
  refresh: function() {
    console.log('刷新菜单目录')
  }
}
var SubMenu = {
  add: function() {
    console.log('增加子菜单')
  },
  del: function(){
    console.log('删除子菜单');
  }
}

这些功能需要封装在对应的命令类中:

// 刷新菜单目录命令类
class RefreshMenuBarCommand {
    constructor(receiver) {
        this.receiver = receiver;
    }

    execute() {
        this.receiver.refresh();
    }
}

// 增加子菜单命令类
class AddSubMenuCommand {
    constructor(receiver) {
        this.receiver = receiver;
    }

    execute() {
        this.receiver.refresh();
    }
}

// '删除子菜单命令类
class DelSubMenuCommand {
    constructor(receiver) {
        this.receiver = receiver;
    }

    execute() {
        this.receiver.refresh();
    }
}

最后就是把命令接收者传入到 command 对象中,并且把 command 对象安装到 button 上面:

var refreshMenuBarCommand = new RefreshMenuBarCommand(MenuBar);
var addSubMenuCommand = new AddSubMenuCommand(SubMenu);
var delSubMenuCommand = new DelSubMenuCommand(SubMenu);

setCommand(button1, refreshMenuBarCommand);
setCommand(button2, addSubMenuCommand);
setCommand(button3, delSubMenuCommand);