设计模式总结(三)

2021-10-09 11:00:0

#设计模式

20

前言

本文是对设计模式总结的第三篇。 主要针对行为型设计模式 第一篇(创建型模式) 第二篇(结构型模式)

正文

发布-订阅模式(观察者模式)

  • 它定义了一种一对多的关系,让多个订阅者对象同时监听某一个发布者,或者叫主题对象,这个主题对象的状态发生改变时就会通知所有订阅自己的订阅者对象,使得它们能够自动更新自己。

生活中的例子

  • 聊天室就是一个典型的发布-订阅模式,当我们加入一个聊天室,相当于订阅了在这个聊天室发送的消息,每当有新的消息产生,聊天室会负责将消息发布给所有聊天室的订阅者。
  • 当我们去 adadis 买鞋,发现看中的款式已经售罄了,售货员告诉你不久后这个款式会进货,到时候打电话通知你。于是你留了个电话,离开了商场,当下周某个时候 adadis 进货了,售货员拿出小本本,给所有关注这个款式的人打电话,这也是一个订阅-发布者模式。
  • 概念

  • Publisher :发布者,当消息发生时负责通知对应订阅者
  • Subscriber :订阅者,当消息发生时被通知的对象
  • SubscriberMap :持有不同 type 的数组,存储有所有订阅者的数组
  • type :消息类型,订阅者可以订阅的不同消息类型
  • subscribe :该方法为将订阅者添加到 SubscriberMap 中对应的数组中
  • unSubscribe :该方法为在 SubscriberMap 中删除订阅者
  • notify :该方法遍历通知 SubscriberMap 中对应 type 的每个订阅者

代码实现

class Publisher {
    constructor() {
        this._subsMap = {}
    }
    
    /* 消息订阅 */
    subscribe(type, cb) {
        if (this._subsMap[type]) {
            if (!this._subsMap[type].includes(cb))
                this._subsMap[type].push(cb)
        } else this._subsMap[type] = [cb]
    }
    
    /* 消息退订 */
    unsubscribe(type, cb) {
        if (!this._subsMap[type] ||
            !this._subsMap[type].includes(cb)) return
        const idx = this._subsMap[type].indexOf(cb)
        this._subsMap[type].splice(idx, 1)
    }
    
    /* 消息发布 */
    notify(type, ...payload) {
        if (!this._subsMap[type]) return
        this._subsMap[type].forEach(cb => cb(...payload))
    }
}

const adadis = new Publisher()

adadis.subscribe('运动鞋', message => console.log('152xxx' + message))    // 订阅运动鞋
adadis.subscribe('运动鞋', message => console.log('138yyy' + message))
adadis.subscribe('帆布鞋', message => console.log('139zzz' + message))    // 订阅帆布鞋

adadis.notify('运动鞋', ' 运动鞋到货了 ~')   // 打电话通知买家运动鞋消息
adadis.notify('帆布鞋', ' 帆布鞋售罄了 T.T') // 打电话通知买家帆布鞋消息

// 输出:  152xxx 运动鞋到货了 ~
// 输出:  138yyy 运动鞋到货了 ~
// 输出:  139zzz 帆布鞋售罄了 T.T

实战例子

  1. Vue的EventBus
  • 和 jQuery 一样,Vue 也是实现有一套事件机制,其中一个我们熟知的用法是 EventBus。在多层组件的事件处理中,如果你觉得一层层 $on、$emit 比较麻烦,而你又不愿意引入 Vuex,那么这时候推介使用 EventBus 来解决组件间的数据通信:
// event-bus.js

import Vue from 'vue'
export const EventBus = new Vue()
使用时:

// 组件A
import { EventBus } from \"./event-bus.js\";

EventBus.$on(\"myevent\", args => {
		console.log(args)
})
// 组件B
import { EventBus } from \"./event-bus.js\";

EventBus.$emit(\"myevent\", 'some args')
  • 实现组件间的消息传递,不过在中大型项目中,还是推介使用 Vuex,因为如果 Bus 上的事件挂载过多,事件满天飞,就分不清消息的来源和先后顺序,对可维护性是一种破坏。
  1. Vue源码中的发布-订阅模式 Description
  • 详情可见Vue 3响应式原理学习笔记
  • 响应式化后的数据相当于发布者
  • 每个组件都对应一个Watcher订阅者。当每个组建的渲染函数被执行时,都会将本组件的Watcher放到自己所依赖的响应式数据的订阅者列表里,这就相当于完成了订阅,这个过程被称为依赖收集(Dependency Collect)。
  • 组件渲染函数执行的结果是生成虚拟DOM树(Virtual DOM Tree),这个树生成后被映射为浏览器上的真实DOM树,也就是用户看到的页面视图。
  • 当响应式数据发生变化的时候,就是出发了setter时,setter会负责通知(Notify)该数据的订阅者列表里的 Watcher,Watcher 会触发组件重渲染(Trigger re-render)来更新(update)视图。
  • 简单来说,响应式数据是消息的发布者,而视图层是消息的订阅者,如果数据更新了,那么发布者就会发布数据更新的消息来通知视图更新,从而实现数据层和视图层的双向绑定。

优点

  • 时间上的解耦:注册的订阅行为由消息的发布方来决定何时调用,订阅者不用持续关注,当消息发生时发布者会负责通知
  • 对象上的解耦:发布者不用提前知道消息的接受者是谁,发布者只需要遍历处理所有订阅该消息类型的订阅者发送消息即可(迭代器模式),由此解耦了发布者和订阅者之间的联系,互不持有,都依赖于抽象,不再依赖于具体
  • 由于其解耦特性,使用场景一帮时:当一个对象的改变需要同时改变其它对象,并且它不知道具体有多少对象需要改变。发布-订阅模式还可以帮助实现一些其他的模式,比如中介者模式。

缺点

  • 增加消耗 :创建结构和缓存订阅者这两个过程需要消耗计算和内存资源,即使订阅后始终没有触发,订阅者也会始终存在于内存
  • 增加复杂度 :订阅者被缓存在一起,如果多个订阅者和发布者层层嵌套,那么程序将变得难以追踪和调试,参考一下 Vue 调试的时候你点开原型链时看到的那堆 deps/subs/watchers 们…
  • 缺点主要在于理解成本、运行效率、资源消耗,特别是在多级发布-订阅时,情况会变得更复杂

其他模式相关

  • 尽管在这里我们把观察者模式和订阅-发布模式当成同一个东西,但是还是有些人认为它们两个是不一样的,特别是在面试的时候问到,我们还是要了解这两者之间的细微插逼的。
  • 观察者模式中的观察者和被观察者之间还存在耦合,被观察者还是知道观察者的
  • 发布-订阅模式 中的发布者和订阅者不需要知道对方的存在,他们通过消息代理来进行通信,解耦更加彻底
  1. 发布-订阅模式和责任链模式
  • 发布-订阅模式:传播的消息是根据需要随时发生变化,是发布者和订阅者之间约定的结构,在多级发布-订阅的场景下,消息可能完全不一样
  • 责任链模式:传播的消息是不变化的,即使变化也是在原来的消息上稍加修正,不会大幅改变结构

策略模式

  • 又称政策模式,其定义一系列的算法,把它们一个个封装起来,并且使它们可以互相替换。封装的策略算法一般是独立的,策略模式根据输入来调整采用哪个算法。关键是策略的实现和使用分离

生活中的例子

  • 如现在的多功能螺丝刀套装,螺丝刀把只需要一个,碰到不同规格的螺丝只要换螺丝刀头就行了,很方便,体积也变小很多
  • 一辆车的轮胎也有很多规格,针对不同的场景车可以更换不同的轮胎,不需要换车
  • 这些都是策略模式的实例,螺丝刀/车属于封装上下文,封装和使用不同的螺丝刀头/轮胎,螺丝刀头/轮胎这里就相当于策略,可以根据需求不同来更换不同的使用策略
  • 如上有下特点
    • 螺丝刀头/轮胎(策略)之间相互独立,但又可以相互替换
    • 螺丝刀/车(封装上下文)可以根据需要的不同选用不同的策略

代码实现

const StrategyMap = {}

function context(type, ...rest) {
    return StrategyMap[type] && StrategyMap[type](...rest)
}

StrategyMap.minus100_30 = function(price) { 
  	return price - Math.floor(price / 100) * 30
}

context('minus100_30', 270)			// 输出: 210
  • 概念
    • Context:封装上下文,根据需要调用策略,屏蔽外界对策略的直接调用,只对外提供一个接口,根据需要调用对应的策略
    • Strategy :策略,含有具体的算法,其方法的外观相同,因此可以互相代替
    • StrategyMap :所有策略的合集,供封装上下文调用

实战例子

  1. 表格formatter
  • Element 的表格控件的 Column 接受一个 formatter 参数,用来格式化内容,其类型为函数,并且还可以接受几个特定参数,像这样: Function(row, column, cellValue, index)
  • 以文件大小转化为例,后端经常会直接传 bit 单位的文件大小,那么前端需要根据后端的数据,根据需求转化为自己需要的单位的文件大小,比如 KB/MB 首先实现文件计算的算法
export const StrategyMap = {
    /* Strategy 1: 将文件大小(bit)转化为 KB */
    bitToKB: val => {
        const num = Number(val)
        return isNaN(num) ? val : (num / 1024).toFixed(0) + 'KB'
    },
    /* Strategy 2: 将文件大小(bit)转化为 MB */
    bitToMB: val => {
        const num = Number(val)
        return isNaN(num) ? val : (num / 1024 / 1024).toFixed(1) + 'MB'
    }
}

/* Context: 生成el表单 formatter */
const strategyContext = function(type, rowKey){ 
  return function(row, column, cellValue, index){
  	StrategyMap[type](row[rowKey])
  }
}

export default strategyContext

在组件中我们就可以这么用了

<template>
    <el-table :data=\"tableData\">
        <el-table-column prop=\"date\" label=\"日期\"></el-table-column>
        <el-table-column prop=\"name\" label=\"文件名\"></el-table-column>
        <!-- 直接调用 strategyContext -->
        <el-table-column prop=\"sizeKb\" label=\"文件大小(KB)\"
                         :formatter='strategyContext(\"bitToKB\", \"sizeKb\")'>
        </el-table-column>
        <el-table-column prop=\"sizeMb\" label=\"附件大小(MB)\"
                         :formatter='strategyContext(\"bitToMB\", \"sizeMb\")'>
        </el-table-column>
    </el-table>
</template>

<script type='text/javascript'>
    import strategyContext from './strategyContext.js'
    
    export default {
        name: 'ElTableDemo',
        data() {
            return {
                strategyContext,
                tableData: [
                    { date: '2019-05-02', name: '文件1', sizeKb: 1234, sizeMb: 1234426 },
                    { date: '2019-05-04', name: '文件2', sizeKb: 4213, sizeMb: 8636152 }]
            }
        }
    }
</script>

<style scoped></style>

2.表单验证

  • ElementUI 的 Form 表单 具有表单验证功能,用来校验用户输入的表单内容。实际需求中表单验证项一般会比较复杂,所以需要给每个表单项增加 validator 自定义校验方法
  • 我们可以像官网示例一样把表单验证都写在组件的状态 data 函数中,但是这样就不好复用使用频率比较高的表单验证方法了,这时我们可以结合策略模式和函数柯里化的知识来重构一下
/* 姓名校验 由2-10位汉字组成 */
export function validateUsername(str) {
    const reg = /^[\\u4e00-\\u9fa5]{2,10}$/
    return reg.test(str)
}

/* 手机号校验 由以1开头的11位数字组成  */
export function validateMobile(str) {
    const reg = /^1\\d{10}$/
    return reg.test(str)
}

/* 邮箱校验 */
export function validateEmail(str) {
    const reg = /^[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+(\\.[a-zA-Z0-9_-]+)+$/
    return reg.test(str)
}
然后在 utils/index.js 中增加一个柯里化方法,用来生成表单验证函数:

// src/utils/index.js

import * as Validates from './validates.js'

/* 生成表格自定义校验函数 */
export const formValidateGene = (key, msg) => (rule, value, cb) => {
    if (Validates[key](value)) {
        cb()
    } else {
        cb(new Error(msg))
    }
}
  • 上面的 formValidateGene 函数接受两个参数,第一个是验证规则,也就是 src/utils/validates.js 文件中提取出来的通用验证规则的方法名,第二个参数是报错的话表单验证的提示信息
<template>
    <el-form ref=\"ruleForm\"
             label-width=\"100px\"
             class=\"demo-ruleForm\"
             :rules=\"rules\"
             :model=\"ruleForm\">
        
        <el-form-item label=\"用户名\" prop=\"username\">
            <el-input v-model=\"ruleForm.username\"></el-input>
        </el-form-item>
        
        <el-form-item label=\"手机号\" prop=\"mobile\">
            <el-input v-model=\"ruleForm.mobile\"></el-input>
        </el-form-item>
        
        <el-form-item label=\"邮箱\" prop=\"email\">
            <el-input v-model=\"ruleForm.email\"></el-input>
        </el-form-item>
    </el-form>
</template>

<script type='text/javascript'>
    import * as Utils from '../utils'
    
    export default {
        name: 'ElTableDemo',
        data() {
            return {
                ruleForm: { pass: '', checkPass: '', age: '' },
                rules: {
                    username: [{
                        validator: Utils.formValidateGene('validateUsername', '姓名由2-10位汉字组成'),
                        trigger: 'blur'
                    }],
                    mobile: [{
                        validator: Utils.formValidateGene('validateMobile', '手机号由以1开头的11位数字组成'),
                        trigger: 'blur'
                    }],
                    email: [{
                        validator: Utils.formValidateGene('validateEmail', '不是正确的邮箱格式'),
                        trigger: 'blur'
                    }]
                }
            }
        }
    }
</script>
  • 可以看见在使用的时候非常方便,把表单验证方法提取出来作为策略,使用柯里化方法动态选择表单验证方法,从而对策略灵活运用,大大加快开发效率

优点

  • 策略直接互相独立,但策略可以自由切换,这个策略模式的特点给策略模式带来很多灵活性,也提高了策略的复用率
  • 如果不采用策略模式,那么在选策略时一般会采用多重的条件查询,采用策略模式可以避免多重条件判断,增加可维护性
  • 可拓展性好,策略可以很方便地进行拓展
  • 适用场景
    • 多个算法只在行为上稍有不同的场景,这时可以使用策略模式来动态选择算法
    • 算法需要自由切换的场景
    • 有时需要多重条件判断,那么可以使用策略模式来规避多重条件判断的情况

缺点

  • 策略相互独立,因此一些复杂的算法逻辑无法共享,造成一些资源浪费
  • 如果用户想采用什么策略,必须了解策略的实现,因此所有策略都需向外暴露,这是违背迪米特法则/最少知识原则的,也增加了用户对策略对象的使用成本

其他相关

  • 策略模式:让我们在程序运行的时候动态地指定要使用的算法
  • 模板方法模式:是在子类定义的时候就已经确定了使用的算法

状态模式

  • 状态模式 (State Pattern)允许一个对象在其内部状态改变时改变它的行为,对象看起来似乎修改了它的类,类的行为随着它的状态改变而改变
  • 当程序需要根据不同的外部情况来做出不同操作时,最直接的方法就是使用 switch-case 或 if-else 语句将这些可能发生的情况全部兼顾到,但是这种做法应付复杂一点的状态判断时就有点力不从心,开发者得找到合适的位置添加或修改代码,这个过程很容易出错,这时引入状态模式可以某种程度上缓解这个问题

生活例子

  • 等红绿灯的时候,红绿灯的状态和行人汽车的通行逻辑是有关联的
    • 红灯亮:行人通行,车辆等待
    • 绿灯亮:行人等待,车辆通行
    • 黄灯亮:行人等待,车辆等待
  • 下载文件时,也有好几个状态
    • 下载验证
    • 下载中
    • 暂停下载
    • 下载完毕
    • 失败
    • 等等等等
  • 如上的场景中的特点可以总结为如下两点
    • 对象有有限多个状态,且状态间可以相互切换
    • 各个状态和对象的行为逻辑有比较强的对应关系,即在不同状态时,对应的处理逻辑不一样 #### 代码实现
/* 抽象状态类 */
class AbstractState {
    constructor() {
        if (new.target === AbstractState) {
            throw new Error('抽象类不能直接实例化!')
        }
    }
    
    /* 抽象方法 */
    employ() {
        throw new Error('抽象方法不能调用!')
    }
}

/* 交通灯类 */
class State extends AbstractState {
    constructor(name, desc) {
        super()
        this.color = { name, desc }
    }
    
    /* 覆盖抽象方法 */
    employ(trafficLight) {
        console.log('交通灯颜色变为 ' + this.color.name + ',' + this.color.desc)
        trafficLight.setState(this)
    }
}

/* 交通灯类 */
class TrafficLight {
    constructor() {
        this.state = null
    }
    
    /* 获取交通灯状态 */
    getState() {
        return this.state
    }
    
    /* 设置交通灯状态 */
    setState(state) {
        this.state = state
    }
}

const trafficLight = new TrafficLight()

const redState = new State('红色', '行人等待 & 车辆等待')
const greenState = new State('绿色', '行人等待 & 车辆通行')
const yellowState = new State('黄色', '行人等待 & 车辆等待')

redState.employ(trafficLight)    // 输出: 交通灯颜色变为 红色,行人通行 & 车辆等待
yellowState.employ(trafficLight) // 输出: 交通灯颜色变为 黄色,行人等待 & 车辆等待
greenState.employ(trafficLight)  // 输出: 交通灯颜色变为 绿色,行人等待 & 车辆通行

// 新建状态的方法
const blueState = new State('蓝色', '行人倒立 & 车辆飞起')
blueState.employ(trafficLight)    // 输出: 交通灯颜色变为 蓝色,行人倒立 & 车辆飞起

原理

  • 所谓对象的状态,通常指的就是对象实例的属性的值。行为指的就是对象的功能,行为大多可以对应到方法上。状态模式把状态和状态对应的行为从原来的大杂烩代码中分离出来,把每个状态所对应的功能处理封装起来,这样选择不同状态的时候,其实就是在选择不同的状态处理类
  • 也就是说,状态和行为是相关联的,它们的关系可以描述总结成:状态决定行为。由于状态是在运行期被改变的,因此行为也会在运行期根据状态的改变而改变,看起来,同一个对象,在不同的运行时刻,行为是不一样的,就像是类被修改了一样
  • 为了提取不同的状态类共同的外观,可以给状态类定义一个共同的状态接口或抽象类,正如之前最后的两个代码示例一样,这样可以面向统一的接口编程,无须关心具体的状态类实现

优点

  • 结构相比之下清晰,避免了过多的 switch-case 或 if-else 语句的使用,避免了程序的复杂性提高系统的可维护性
  • 符合开闭原则,每个状态都是一个子类,增加状态只需增加新的状态类即可,修改状态也只需修改对应状态类就可以了
  • 封装性良好,状态的切换在类的内部实现,外部的调用无需知道类内部如何实现状态和行为的变换
  • 适用场景
    • 操作中含有庞大的多分支的条件语句,且这些分支依赖于该对象的状态,那么可以使用状态模式来将分支的处理分散到单独的状态类中
    • 对象的行为随着状态的改变而改变,那么可以考虑状态模式,来把状态和行为分离,虽然分离了,但是状态和行为是对应的,再通过改变状态调用状态对应的行为

缺点

  • 引入了多余的类,每个状态都有对应的类,导致系统中类的个数增加

其他相关

  • 状态模式:
    • 重在强调对象内部状态的变化改变对象的行为,状态类之间是平行的,无法相互替换
    • 根据状态来分离行为,当状态发生改变的时候,动态地改变行为
  • 策略模式: 策略的选择由外部条件决定,策略可以动态的切换,策略之间是平等的,可以相互替换
  • 状态模式的状态类是平行的,意思是各个状态类封装的状态和对应的行为是相互独立、没有关联的,封装的业务逻辑可能差别很大毫无关联,相互之间不可替换。但是策略模式中的策略是平等的,是同一行为的不同描述或者实现,在同一个行为发生的时候,可以根据外部条件挑选任意一个实现来进行处理
  • 发布-订阅模式: 发布者在消息发生时通知订阅者,具体如何处理则不在乎,或者直接丢给用户自己处理

模版方法模式

  • 模板方法模式(Template Method Pattern)父类中定义一组操作算法骨架,而将一些实现步骤延迟到子类中,使得子类可以不改变父类的算法结构的同时,重新定义算法中的某些实现步骤。模板方法模式的关键是算法步骤的骨架和具体实现分离。

生活例子

做菜的过程可以被总结为固定的几个步骤

  • 准备食材(肉、蔬菜、菌菇)
  • 食材放到锅里
  • 放调味料(糖、盐、油)
  • 炒菜
  • 倒到容器里(盘子、碗) 在这种情况下我们有将流程抽象的余地了
  • 这个流程我们可以抽象出来,由具体实例的操作流程来实现,比如做咖啡的时候冲泡的就是咖啡,做茶的时候冲泡的就是茶
  • 一些共用的流程,就可以使用通用的公共步骤,比如把水煮沸,比如将食材放到锅里,这样的共用流程就可以共用一个具体方法就可以了

代码实现

/* 抽象父类 */
class AbstractClass {
    constructor() {
        if (new.target === AbstractClass) {
            throw new Error('抽象类不能直接实例化!')
        }
    }
    
    /* 共用方法 */
    operate1() { console.log('operate1') }
    
    /* 抽象方法 */
    operate2() { throw new Error('抽象方法不能调用!') }
    
    /* 模板方法 */
    templateMethod() {
        this.operate1()
        this.operate2()
    }
}

/* 实例子类,继承抽象父类 */
class ConcreteClass extends AbstractClass {
    constructor() { super() }
    
    /* 覆盖抽象方法 operate2 */
    operate2() { console.log('operate2') }
}

const instance = new ConcreteClass()
instance.templateMethod()

// 输出:operate1
// 输出:operate2

优点

  • 封装了不变部分,扩展可变部分, 把算法中不变的部分封装到父类中直接实现,而可变的部分由子类继承后再具体实现; 提取了公共代码部分,易于维护, 因为公共的方法被提取到了父类,那么如果我们需要修改算法中不变的步骤时,不需要到每- 一个子类中去修改,只要改一下对应父类即可
  • 行为被父类的模板方法固定, 子类实例只负责执行模板方法,具备可扩展性,符合开闭原则
  • 使用场景
    • 如果知道一个算法所需的关键步骤,而且很明确这些步骤的执行顺序,但是具体的实现是未知的、灵活的,那么这时候就可以使用模板方法模式来将算法步骤的框架抽象出来
    • 重要而复杂的算法,可以把核心算法逻辑设计为模板方法,周边相关细节功能由各个子类实现
    • 模板方法模式可以被用来将子类组件将自己的方法挂钩到高层组件中,也就是钩子,子类组件中的方法交出控制权,高层组件在模板方法中决定何时回调子类组件中的方法,类似的用法场景还有发布-订阅模式、回调函数

缺点

  • 增加了系统复杂度,主要是增加了的抽象类和类间联系,需要做好文档工作

其他相关

  • 抽象工厂模式:提取的是实例的功能结构
  • 模板方法模式:提取的是算法的骨架结构

迭代器模式

  • 迭代器模式 (Iterator Pattern)用于顺序地访问聚合对象内部的元素,又无需知道对象内部结构。使用了迭代器之后,使用者不需要关心对象的内部构造,就可以按序访问其中的每个元素。
  • 如JavaScript中的forEach就是一个迭代器
  • 想要自己实现一个迭代器也很简单
var forEach = function(arr, cb) {
    for (var i = 0; i < arr.length; i++) {
        cb.call(arr[i], arr[i], i, arr)
    }
}

forEach(['hello', 'world', '!'], function(currValue, idx, arr) {
    console.log('当前值 ' + currValue + ',索引为 ' + idx)
})

// 输出: 当前值 hello,索引为 0
// 输出: 当前值 world,索引为 1
// 输出: 当前值 !    ,索引为 2

JavaScript的原生支持

  • 随着 JavaScript 的 ECMAScript 标准每年的发展,给越来越多好用的 API 提供了支持,比如 Array 上的 filter、forEach、reduce、flat 等,还有 Map、Set、String 等数据结构,也提供了原生的迭代器支持,给我们的开发提供了很多便利,也让 underscore 这些工具库渐渐淡出历史舞台 另外,JavaScript 中还有很多类数组结构,比如:
  • arguments:函数接受的所有参数构成的类数组对象
  • NodeList:是 querySelector 接口族返回的数据结构
  • HTMLCollection:是 getElementsBy 接口族返回的数据结构
  • 对于如上的数组结构,可以用一些方法转化为普通数组结构,如arguments
// 方法一
var args = Array.prototype.slice.call(arguments)

// 方法二
var args = [].slice.call(arguments)

// 方法三 ES6提供
const args = Array.from(arguments)

// 方法四 ES6提供
const args = [...arguments];
  • 转化为数组后就可以使用各种Array自带的api了

ES6中的迭代器

  • ES6 规定,默认的迭代器部署在对应数据结构的 Symbol.iterator 属性上,如果一个数据结构具有 Symbol.iterator 属性,就被视为可遍历的,就可以用 for...of 循环遍历它的成员。也就是说,for...of循环内部调用的是数据结构的Symbol.iterator 方法
  • for-of 循环可以使用的范围包括 Array、Set、Map 结构、上文提到的类数组结构、Generator 对象,以及字符串
  • 详情可见 阮一峰 ECMAScript 6 (ES6) 标准入门教程 第三版
  • 通过 for-of 可以使用 Symbol.iterator 这个属性提供的迭代器可以遍历对应数据结构,如果对没有提供 Symbol.iterator 的目标使用 for-of 则会抛错
  • 也可以手动给一个对象设置迭代器,让一个对象也可以使用 for-of 循环

总结

  • 迭代器模式早已融入我们的日常开发中,在使用 filter、reduce、map 等方法的时候,不要忘记这些便捷的方法就是迭代器模式的应用。当我们使用迭代器方法处理一个对象时,我们可以关注与处理的逻辑,而不必关心对象的内部结构,侧面将对象内部结构和使用者之间解耦,也使得代码中的循环结构变得紧凑而优美

命令模式

  • 命令模式 (Command Pattern)又称事务模式,将请求封装成对象,将命令的发送者和接受者解耦。本质上是对方法调用的封装
  • 通过封装方法调用,也可以做一些有意思的事,例如记录日志,或者重复使用这些封装来实现撤销(undo)、重做(redo)操作

生活例子

  • 比较典型的就是餐馆订餐,客人需要向厨师发送请求,但是不知道这些厨师的联系方式,也不知道厨师炒菜的流程和步骤,一般是将客人订餐的请求封装成命令对象,也就是订单。这个订单对象可以在程序中被四处传递,就像订单可以被服务员传递到某个厨师手中,客人不需要知道是哪个厨师完成自己的订单,厨师也不需要知道是哪个客户的订单。
  • 另外游戏也经常使用这种命令模式,按下对应的键位或鼠标,便向游戏人物发出对应的命令,游戏人物收到命令后便在游戏画面上呈现出相关的动作。
  • 特点:
    • 命令的发送者和接收者解耦,发送者与接收者之间没有直接引用关系,发送请求的对象只需要知道如何发送请求,而不必知道如何完成请求
    • 对命令还可以进行撤销、排队等操作,比如用户等太久不想等了撤销订单,厨师不够了将订单进行排队,等等操作

代码实现

  • 实现一个操作游戏人物的代码
// 向上移动命令对象
const MoveUpCommand = {
    execute(role) {
        role.move(0, -CanvasStep)
    },
    undo(role) {
        role.move(0, CanvasStep)
    }
}

// 向下移动命令对象
const MoveDownCommand = {
    execute(role) {
        role.move(0, CanvasStep)
    },
    undo(role) {
        role.move(0, -CanvasStep)
    }
}

// 向左移动命令对象
const MoveLeftCommand = {
    execute(role) {
        role.move(-CanvasStep, 0)
    },
    undo(role) {
        role.move(CanvasStep, 0)
    }
}

// 向右移动命令对象
const MoveRightCommand = {
    execute(role) {
        role.move(CanvasStep, 0)
    },
    undo(role) {
        role.move(-CanvasStep, 0)
    }
}

// 命令管理者
const CommandManager = {
    undoStack: [],   // 撤销命令栈
    redoStack: [],   // 重做命令栈
    
    executeCommand(role, command) {
        this.redoStack.length = 0    // 每次执行清空重做命令栈
        this.undoStack.push(command) // 推入撤销命令栈
        command.execute(role)
    },
    
    /* 撤销 */
    undo(role) {
        if (this.undoStack.length === 0) return
        const lastCommand = this.undoStack.pop()
        lastCommand.undo(role)
        this.redoStack.push(lastCommand)  // 放入redo栈中
    },
    
    /* 重做 */
    redo(role) {
        if (this.redoStack.length === 0) return
        const lastCommand = this.redoStack.pop()
        lastCommand.execute(role)
        this.undoStack.push(lastCommand)  // 放入undo栈中
    }
}

// 设置按钮命令
const setCommand = function(element, role, command) {
    if (typeof command === 'object') {
        element.onclick = function() {
            CommandManager.executeCommand(role, command)
        }
    } else {
        element.onclick = function() {
            command.call(CommandManager, role)
        }
    }
}

/* ----- 客户端 ----- */
const mario = new Role(200, 200, 'https://i.loli.net/2019/08/09/sqnjmxSZBdPfNtb.jpg')

setCommand(btnUp, mario, MoveUpCommand)
setCommand(btnDown, mario, MoveDownCommand)
setCommand(btnLeft, mario, MoveLeftCommand)
setCommand(btnRight, mario, MoveRightCommand)

setCommand(btnUndo, mario, CommandManager.undo)
setCommand(btnRedo, mario, CommandManager.redo)

优点

  • 命令模式将调用命令的请求对象与执行该命令的接收对象解耦,因此系统的可扩展性良好,加入新的命令不影响原有逻辑,所以增加新的命令也很容易
  • 命令对象可以被不同的请求者角色重用,方便复用
  • 可以将命令记入日志,根据日志可以容易地实现对命令的撤销和重做
  • 使用场景
    • 需要将请求调用者和请求的接收者解耦的时候
    • 需要将请求排队、记录请求日志、撤销或重做操作时

缺点

  • 命令类或者命令对象随着命令的变多而膨胀,如果命令对象很多,那么使用者需要谨慎使用,以免带来不必要的系统复杂度

其他相关

  • 命令模式和职责链模式可以结合使用,比如具体命令的执行,就可以引入职责链模式,让命令由职责链中合适的处理者执行
  • 命令模式和组合模式可以结合使用,比如不同的命令可以使用组合模式的方法形成一个宏命令,执行完一个命令之后,再继续执行其子命令
  • 命令模式与工厂模式可以结合使用,比如命令模式中的命令可以由工厂模式来提供

职责链模式

生活例子

  • 当你作为请求者提出请假申请时,这个申请会由小组领导、部门经理、总经理之中的某一位领导来进行处理,但一开始提出申请的时候,并不知道这个申请之后由哪个领导来处理,也许是部门经理,或者是总经理,请求者事先不知道这个申请最后到底应该由哪个领导处理

特点

  • 请求在一系列对象中传递,形成一条链
  • 链中的请求接受者对请求进行分析,要么处理这个请求,要么把这个请求传递给链的下一个接受者

代码实现

/* 领导基类 */
class Leader {
    constructor() {
        this.nextLeader = null
    }
    
    setNext(next) {
        this.nextLeader = next
        return next
    }
}

/* 小组领导 */
class GroupLeader extends Leader {
    handle(duration) { ... }
}

/* 部门领导 */
class DepartmentLeader extends Leader {
    handle(duration) { ... }
}

/* 总经理 */
class GeneralLeader extends Leader {
    handle(duration) { ... }
}

const zhangSan = new GroupLeader()
const liSi = new DepartmentLeader()
const wangWu = new GeneralLeader()

/* 组装职责链 */
zhangSan
  .setNext(liSi)     // 设置小组领导的下一个职责节点为部门领导
  .setNext(wangWu)   // 设置部门领导的下一个职责节点为总经理

原理

  • 职责链模式可能在真实的业务代码中见的不多,但是作用域链、原型链、DOM 事件流的事件冒泡,都有职责链模式的影子
    • 作用域链: 查找变量时,先从当前上下文的变量对象中查找,如果没有找到,就会从父级执行上下文的变量对象中查找,一直找到全局上下文的变量对象
    • 原型链: 当读取实例的属性时,如果找不到,就会查找当前对象关联的原型中的属性,如果还查不到,就去找原型的原型,一直找到最顶层为止
    • 事件冒泡: 事件在 DOM 元素上触发后,会从最内层的元素开始发生,一直向外层元素传播,直到全局 document 对象
      • 以事件冒泡为例,事件在某元素上触发后,会一级级往外层元素传递事件,如果当前元素没有处理这个事件并阻止冒泡,那么这个事件就会往外层节点传递,就像请求在职责链中的职责节点上传递一样,直到某个元素处理了事件并阻止冒泡 Description

优点

  • 和命令模式类似,由于处理请求的职责节点可能是职责链上的任一节点,所以请求的发送者和接受者是解耦的
  • 通过改变链内的节点或调整节点次序,可以动态地修改责任链,符合开闭原则
  • 适用场景:
    • 需要多个对象可以处理同一个请求,具体该请求由哪个对象处理在运行时才确定
    • 在不明确指定接收者的情况下,向多个对象中的其中一个提交请求的话,可以使用职责链模式
    • 如果想要动态指定处理一个请求的对象集合,可以使用职责链模式

缺点

  • 并不能保证请求一定会被处理,有可能到最后一个节点还不能处理
  • 调试不便,调用层次会比较深,也有可能会导致循环引用

其他相关

  • 职责链模式与组合模式
    • 职责链模式可以和组合模式一起使用,比如把职责节点通过组合模式来组合,从而形成组合起来的树状职责链
  • 职责链模式与装饰模式
    • 这两个模式都是在运行期间动态组合,装饰模式是动态组合装饰器,可以有任意多个对象来装饰功能,而职责链是动态组合职责节点,有一个职责节点处理的话就结束
    • 目的不同:装饰模式为对象添加功能,而职责链模式是要实现发送者和接收者解耦

中介者模式

  • 中介者模式 (Mediator Pattern)又称调停模式,使得各对象不用显式地相互引用,将对象与对象之间紧密的耦合关系变得松散,从而可以独立地改变他们。核心是多个对象之间复杂交互的封装
  • 根据最少知识原则,一个对象应该尽量少地了解其他对象。如果对象之间耦合性太高,改动一个对象则会影响到很多其他对象,可维护性差。复杂的系统,对象之间的耦合关系会得更加复杂,中介者模式就是为了解决这个问题而诞生的

代码实现

  • Colleague: 同事对象,只知道中介者而不知道其他同事对象,通过中介者来与其他同事对象通信
  • Mediator: 中介者,负责与各同事对象的通信
/**
 * 多个玩家堆栈
 * 我们将partners和enemies作为玩家的属性,不使用中介者模式的话每当玩家有什么动作,比如角色移动、吃到道具、玩家死亡,
 * 我们都要显示的遍历其他对象。现在只是两个队,如果有成千上万的玩家,几十个队伍,如果有一个人掉线,就必须从
 * 所有的玩家以及敌人列表中移除。如果有解除队伍,加入别的队伍的功能,就不是循环解决的问题了。
 * 下面使用中介者模式来解决的话,只需要告知中介者,由中介者来处理并告知其他对象便可:
 */
var Player = function(name, teamColor) {
    this.name = name;
    this.teamColor = teamColor;
    this.state = \"alive\";
};
Player.prototype.lose = function() {
    console.log(this.name + \": lost.\");
};
Player.prototype.win = function() {
    console.log(this.name + \": win.\");
};
Player.prototype.die = function() {
    playerDirector.receiveMessage(\"playerDead\", this);
};
Player.prototype.remove = function() {
    playerDirector.receiveMessage(\"playerRemove\", this);
};
Player.prototype.changeTeam = function(color) {
    playerDirector.receiveMessage(\"changeTeam\", this, color);
};
var playerDirector = (function() {
    var players = {},
        operations = {};
    operations.addPlayer = function(player) {
        var teamColor = player.teamColor;
        players[teamColor] = players[teamColor] || [];
        players[teamColor].push(player);
    };
    operations.playerRemove = function(player) {
        var teamColor = player.teamColor;
        var arr = players[teamColor];
        if(!arr || arr.length < 1) {
            return;
        }
        for(var i = 0, len = arr.length; i < len; i++) {
            if(arr[i] === player) {
                arr.splice(i, 1);
                break;
            }
        }
    };
    operations.changeTeam = function(player, newTeamColor) {
        operations.playerRemove(player);
        player.teamColor = newTeamColor;
        operations.addPlayer(player);
    };
    operations.playerDead = function(player) {
        var all_dead = true;
        var teamColor = player.teamColor,
            teamPlayers = players[teamColor];
        player.state = \"dead\";
        for(var a of teamPlayers) {
            if(\"dead\" !== a.state) {
                all_dead = false;
                break;
            }
        }
        if(all_dead) {
            for(var a of teamPlayers) { // 本队队友全部死亡
                a.lose();
            }
            for(var b in players) {
                if(teamColor !== b) { // 告知敌人游戏胜利
                    for(var p of players[b]) {
                        p.win();
                    }
                }
            }
        }
    };
    var receiveMessage = function() {
        var key = Array.prototype.shift.call(arguments);
        if(!operations[key]) {
            console.log(\"无效的命令:\" + key);
            return;
        }
        operations[key].apply(this, arguments);
    };
    return {
        receiveMessage: receiveMessage
    }
})();
var PlayerFactory = function(name, color) {
    var player = new Player(name, color);
    playerDirector.receiveMessage(\"addPlayer\", player);
    return player;
};

var player1 = PlayerFactory(\"player1\", \"red\"),
    player2 = PlayerFactory(\"player2\", \"red\"),
    player3 = PlayerFactory(\"player3\", \"red\"),
    player4 = PlayerFactory(\"player4\", \"red\");
    
var player5 = PlayerFactory(\"player5\", \"blue\"),
    player6 = PlayerFactory(\"player6\", \"blue\"),
    player7 = PlayerFactory(\"player7\", \"blue\"),
    player8 = PlayerFactory(\"player8\", \"blue\");
    
player1.changeTeam(\"blue\");
player2.die();
player3.die();
player4.die();

优点

  • 松散耦合,降低了同事对象之间的相互依赖和耦合,不会像之前那样牵一发动全身
  • 将同事对象间的一对多关联转变为一对一的关联,符合最少知识原则,提高系统的灵活性,使得系统易于维护和扩展
  • 中介者在同事对象间起到了控制和协调的作用,因此可以结合代理模式那样,进行同事对象间的访问控制、功能扩展
  • 因为同事对象间不需要相互引用,因此也可以简化同事对象的设计和实现
  • 适用场景
    • 适用多个对象间的关系确实已经紧密耦合,且导致扩展、维护产生了困难的场景,也就是当多个对象之间的引用关系变成了网状结构的时候,此时可以考虑使用引入中介者来把网状结构转化为星型结构
    • 如果对象之间关系耦合并不紧密,或者之间的关系本就一目了然,这时引入中介者模式就是多此一举
    • MVC/MVVM框架就含有中介者模式的思想,Controller/ViewModel 层作为中介者协调 View/Model 进行工作,减少 View/Model 之间的直接耦合依赖,从而做到视图层和数据层的最大分离

缺点

  • 逻辑过度集中化,当同事对象太多时,中介者的职责将很重,逻辑变得复杂而庞大,以至于难以维护

其他相关

  • 中介者模式与发布-订阅模式
    • 中介者模式和发布-订阅模式都可以用来进行对象间的解耦,比如发布-订阅模式的发布者/订阅者和中介者模式里面的中介者/同事对象功能上就比较类似
    • 这两个模式也可以组合使用,比如中介者模式就可以使用发布-订阅模式,对相关同事对象进行消息的广播通知
    • 比如上面相亲的例子中,注册各方和通知信息就使用了发布-订阅模式
  • 中介者模式与代理模式
    • 同事对象之间需要通信的时候,需要经由中介者,这时中介者就相当于同事对象间的代理。所以这时就可以引入代理模式的概念,对同事对象相互访问的时候,起到访问控制、功能扩展等等功能