(十三)其他设计模式

138 阅读10分钟

@TOC

其他设计模式

优先级划分依据

  • 不常用
  • 是找不到经典应用场景

设计模式有:

创建型

  • 原型模式

结构型

  • 桥接模式
  • 组合模式
  • 享元模式

行为型

  • 策略模式 、模板方法模式 、职责链模式
  • 命令模式 、备忘录模式 、中介者模式
  • 访问者模式 、解释器模式

原型模式

  • clone自己,生成一个新对象
  • java默认有clone接口,不用自己实现

基于一个对象创建一个重复的对象,重新new代价较大的情况下,就拷贝一份。具体实现就是自己实现一个 clone 自己的 API ,外部调用时克隆自己。如 java 内置了clone方法,不用自己定义。

JS 用到不多,因为 JS 中基本不会遇到new代价很大的场景。基本遇到需要创建时,直接重新new就可以了。

不过Object.create用到了原型模式的思想(虽然不是 java 中的 clone ),基于一个原型创建一个对象

// `Object.create` 用到了原型模式的思想(虽然不是 java 中的 clone )
// 基于一个原型创建一个对象
var prototype = {
    getName: function () {
        return this.first + ' ' + this.last
    },
    say: function () {
        console.log('hello')
    }
}

// 基于原型创建 x
var x = Object.create(prototype)
x.first = 'A'
x.last = 'B'
console.log(x.getName())
x.say()

// 基于原型创建 y
var y = Object.create(prototype)
y.first = 'C'
y.last = 'D'
console.log(y.getName())
y.say()

对比js中的原型prototype

  • prototype可以理解为ES6 class的一种底层原理
  • 而class是实现面向对象的基础,而不是服务于某个模式
  • 若干年后ES6全面普及,大家可能会忽略掉prototype
  • 但是Object create却会长久存在

和 js 的 prototype 不一样。在 ES6 语法中,全部使用 class ,prototype 就成了一种底层的实现机制,而不再对开发人员开放。可能若干年后,ES6 标准统一天下,倒是很多初学者都不知道 prototype 是怎么回事儿。因此,目前 JS prototype 可以看做和 class 是同样的功能,并不是原型模式。

prototype 将会隐藏,而 Object.create 将会长久保留。

桥接模式

  • 用于把抽象化与实现化解耦
  • 使得二者可以独立变化
  • (未找到JS中的经典应用)

桥接(Bridge)是用于把抽象化与实现化解耦,使得二者可以独立变化。在常见的 JS 代码中找不到合适的例子,下面例子比较合适。

画有颜色的图形,第一种设计方式如下

在这里插入图片描述

示例代码

class ColorShape {
    yellowCircle() {
        console.log('yellow circle')
    }
    redCircle() {
        console.log('red circle')
    }
    yellowTriangle() {
        console.log('yellow triangle')
    }
    redTriangle() {
        console.log('red triangle')
    }
}

// 测试
let cs = new ColorShape()
cs.yellowCircle()
cs.redCircle()
cs.yellowTriangle()
cs.redTriangle()

第二种设计方式

在这里插入图片描述

示例代码

class Color {
    constructor(name) {
        this.name = name
    }
}
class Shape {
    constructor(name, color) {
        this.name = name
        this.color = color
    }
    draw() {
        console.log(`${this.color.name} ${this.name}`)
    }
}

// 测试代码
let red = new Color('red')
let yellow =  new Color('yello')
let circle = new Shape('circle', red)
circle.draw()
let triangle = new Shape('triangle', yellow)
triangle.draw()

显然,第二种方式更加符合开放封闭原则。

另外一个原则 —— 少继承,多聚合

设计原则验证

  • 抽象与实现分离,解耦

  • 符合开放封闭原则

组合模式

  • 生成树形结构,表示“整体-部分”关系
  • 让整体和部分都具有一致的操作方式

组合模式,将对象组合成树形结构以表示“部分-整体”的层次结构,组合模式使得用户对单个对象和组合对象的使用具有一致性。

按照传统组合模式中数据结构应该是这样的

例子

在这里插入图片描述

  • JS经典应用中,未找到这么复杂的数据类型,
  • 虚拟 DOM 中的 vnode 结构是这种形式,但数据类型简单。
  • (用JS实现一个菜单,不算经典应用,与业务相关)
<div id="div1" class="container">
    <p>123</p>
    <p>456</p>
</div>

以上结构可被定义为

{
    tag: 'div',
    attr: {
        id: 'div1',
        className: 'container'
    },
    children: [
        {
            tag: 'p',
            attr: {},
            children: ['123']
        },
        {
            tag: 'p',
            attr: {},
            children: ['456']
        }
    ]
}
  • 整体和单个节点的操作是一致的

  • 整体和单个节点的数据结构也保持一致

关键的一点,对于整体还是单个节点的操作都是一致的。例如,以上结构无论是拿到div节点还是p节点,数据结构都是一样的。

设计原则验证 将整体和单个节点的操作抽象出来 符合开放封闭原则

享元模式

  • 共享内存(主要考虑内存,而非效率)

  • 相同的数据,共享使用

  • (JS中未找到经典应用场景)

享元模式(Flyweight),运行共享技术有效地支持大量细粒度的对象,避免大量拥有相同内容的小类的开销(主要考虑空间效率,如内存),使大家共享一个类(元类) 。

就是为了避免开销过大而共享一些数据,未能找到 JS 中特别符合的例子。但是符合享元模式设计思想的例子有:

无限下拉列表,将事件代理到高层节点上,如下代码。如果都绑定到<a>标签,对内存开销太大。

<!-- 无限下拉列表,将事件代理到高层节点上 -->
<!-- 如果都绑定到`<a>`标签,对内存开销太大 -->
<div id="div1">
    <a href="#">a1</a>
    <a href="#">a2</a>
    <a href="#">a3</a>
    <a href="#">a4</a>
    <!-- 无限下拉列表 -->
</div>

<script>
    var div1 = document.getElementById('div1')
    div1.addEventListener('click', function (e) {
        var target = e.target
        if (e.nodeName === 'A') {
            alert(target.innerHTML)
        }
    })
</script>

注意,在此前的代理模式中也用到这个 demo ,不过两者不冲突,体现的是两种设计思想而已。

设计原则验证 将相同的部分抽象出来 符合开放封闭原则

策略模式

  • 不同策略分开处理
  • 避免出现大量if...else或者switch...case
  • (JS中未找到经典应用场景)

主要解决多个if...else或者switch...case的问题。

多用于具体的业务代码中,因为本教程没有做一个实战项目,因此先举个例子说明一下。例如一次购买行为,针对普通用户、会员和 vip 都有不同的折扣。普通的实现方式如:

class User {
    constructor(type) {
        this.type = type
    }
    buy() {
        if (this.type === 'ordinary') {
            console.log('普通用户购买')
        } else if (this.type === 'member') {
            console.log('会员用户购买')
        } else if (this.type === 'vip') {
            console.log('vip 用户购买')
        }
    }
}

// 测试代码
var u1 = new User('ordinary')
u1.buy()
var u2 = new User('member')
u2.buy()
var u3 = new User('vip')
u3.buy()

使用策略模式之后

class OrdinaryUser {
    buy() {
        console.log('普通用户购买')
    }
}
class MemberUser {
    buy() {
        console.log('会员用户购买')
    }
}
class VipUser {
    buy() {
        console.log('vip 用户购买')
    }
}

var u1 = new OrdinaryUser()
u1.buy()
var u2 = new MemberUser()
u2.buy()
var u3 = new VipUser()
u3.buy()

关键在于:把 if...else 拆分开,分出不同的策略,每个策略单独处理,而不是混在一起

设计原则验证 不同策略,分开处理,而不是混合在一起 符合开放封闭原则

模板方法模式

模板方法模式想要表述的其实特别简单,就是将分散的一些操作集中起来,例如

class Action {
    handle() {
        handle1()
        handle2()
        handle3()
    }
    handle1() {
        console.log('1')
    }
    handle2() {
        console.log('2')
    }
    handle3() {
        console.log('3')
    }
}

职责链模式

  • 一步操作可能分位多个职责角色来完成
  • 把这些角色都分开,然后用一个链串起来
  • 将发起者和处理者、包括多个处理者之间进行了分离

例如一个请假审批,需要组长审批、经理审批、最后总监审批。代码如下:

// 请假审批,需要组长审批、经理审批、最后总监审批
class Action {
    constructor(name) {
        this.name = name
        this.nextAction = null
    }
    setNextAction(action) {
        this.nextAction = action
    }
    handle() {
        console.log(`${this.name} 审批`)
        if (this.nextAction != null) {
            this.nextAction.handle()
        }
    }
}

let a1 = new Action('组长')
let a2 = new Action('经理')
let a3 = new Action('总监')
a1.setNextAction(a2)
a2.setNextAction(a3)
a1.handle()

职责连模式概念上的用意是:请求者发起请求,但是不知道哪个审批者会审批,因此就弄一个链来操作,总有一个节点会审批。例如你请假,组长、经理、和总监最终肯定会有一个人来绝对你能否请假成功。

由此我们可以联想到 JS 中的链式操作 。JS 的链式操作只是一种技术上的操作手段,职责连模式要结合业务。即,链式操作可以实现职责连模式,也可以不用职责连模式,这取决于业务的需要。

  • jQuery 的链式操作
  • Promise.then 的链式操作
  • Stream pipe 链式操作

设计原则验证 发起者与各个处理者进行隔离 符合开放封闭原则

命令模式

  • 执行命令时,发布者和执行者分开

  • 中间加入命令对象,作为中转站

执行一个命令时,将命令的触发者和执行者分开,不让触发者直接操作命令执行者。

在这里插入图片描述

代码演示

class Receiver {
    exec() {
        console.log('执行')
    }
}
class Command {
    constructor(receiver) {
        this.receiver = receiver
    }
    cmd() {
        console.log('触发命令')
        this.receiver.exec()
    }
}
class Invoker {
    constructor(command) {
        this.command = command
    }
    invoke() {
        console.log('开始')
        this.command.cmd()
    }
}

// 士兵
let soldier = new Receiver()
// 小号手
let trumpeter = new Command(soldier)
// 将军
let general = new Invoker(trumpeter)
general.invoke()

JS中的应用 网页富文本编辑器操作,浏览器封装了一个命令对象 document.execCommand('bold') document.execCommand('undo') 在这里插入图片描述

实际的例子不是很多,可列举一个。要做一个 web 富文本编辑器,需要 JS 操作文本的样式,例如要对选中的文本进行加粗,需要执行document.execCommand('bold')。这个 API 用到的就是命令模式,即我作为使用者是需要下达一个bold命令即可,如何执行我不用关心。撤下就执行document.execCommand('undo'),恢复就执行document.execCommand('redo')

如何做到撤销和恢复,可参考备忘录模式。

设计原则验证 命令对象与执行对象分开,解耦 符合开放封闭原则

备忘录模式

  • 随时记录一个对象的状态变化
  • 随时可以恢复之前的某个状态(如撤销功能)
  • (未找到JS中经典应用),除了一些工具(如编辑器)

保存一个对象的某个状态,以便在适当的时候恢复对象。例如撤销功能,日常页面用的不多,除非一些工具(如编辑器)

代码演示一下该模式的应用:

// 状态备忘
class Memento {
    constructor(content) {
        this.content = content
    }
    getContent() {
        return this.content
    }
}

// 备忘列表
class CareTaker {
    constructor() {
        this.list = []
    }
    add(memento) {
        this.list.push(memento)
    }
    get(index) {
        return this.list[index]
    }
}

// 编辑器
class Editor {
    constructor() {
        this.content = null
    }
    setContent(content) {
        this.content = content
    }
    getContent() {
        return this.content
    }
    saveContentToMemento() {
        return new Memento(this.content)
    }
    getContentFromMemento(memento) {
        this.content = memento.getContent()
    }
}

// 测试代码
let editor = new Editor()
let careTaker = new CareTaker()
editor.setContent('111')
editor.setContent('222')
careTaker.add(editor.saveContentToMemento()) // 存储备忘录
editor.setContent('333')
careTaker.add(editor.saveContentToMemento()) // 存储备忘录
editor.setContent('444')

console.log(editor.getContent())
editor.getContentFromMemento(careTaker.get(1)) // 撤销
console.log(editor.getContent())
editor.getContentFromMemento(careTaker.get(0))  // 撤销
console.log(editor.getContent())

编辑器是一个比较小众的工具,很少有人开发这个。

设计原则验证 状态对象与使用者分开,解耦 符合开放封闭原则

访问者模式

将数据操作和数据结构进行分离,使用频率不高,就不做具体解释了。

中介者模式

普通的多对象通讯的场景与中介者模式

在这里插入图片描述

代码演示:

class A {
    constructor() {
        this.number = 0
    }
    setNumber(num, b) {
        this.number = num
        if (b) {
            b.setNumber(num * 100)
        }
    }
}

class B {
    constructor() {
        this.number = 0
    }
    setNumber(num, a) {
        this.number = num
        if (a) {
            a.setNumber(num / 100)
        }
    }
}

// 测试代码
let a = new A()
let b = new B()
a.setNumber(100, b)
console.log(a.number, b.number)  // 100 10000
b.setNumber(100, a)
console.log(a.number, b.number)  // 1 100

以上代码中,ab产生了耦合关系。使用了之后:

代码演示:

class Mediator {
    constructor(a, b) {
        this.a = a
        this.b = b
    }
    setA() {
        let number = this.b.number
        this.a.setNumber(number * 100)
    }
    setB() {
        let number = this.a.number
        this.b.setNumber(number / 100)
    }
}

class A {
    constructor() {
        this.number = 0
    }
    setNumber(num, m) {
        this.number = num
        if (m) {
            m.setB()
        }
    }
}

class B {
    constructor() {
        this.number = 0
    }
    setNumber(num, m) {
        this.number = num
        if (m) {
            m.setA()
        }
    }
}

// 测试代码
let a = new A()
let b = new B()
let m = new Mediator(a, b)
a.setNumber(100, m)
console.log(a.number, b.number)  // 100 10000
b.setNumber(100, m)
console.log(a.number, b.number)  // 1 100

这个设计模式应该更加体现到业务代码中,目前没有找到特别合适的讲解示例。

设计原则验证 将各关联对象通过中介者隔离 符合开放封闭原则

解释器模式

  • 描述语言语法如何定义,如何解释和编译

  • 用于专业场景

解释器模式描述了如何为简单的语言定义一个文法,如何在该语言中表示一个句子,以及如何解析这些句子。

和语言的解析与编译有关,不常用。除非你想深入这部分的学习,例如想详细了解如何实现一个正则表达式。

关于面试

能说出课程重点讲解的设计模式即可

日常使用

重点讲解的设计模式,要强制自己模仿、掌握 非常用的设计模式,视业务场景选择性使用