# 设计模式-总览

【参考】 慕课网-Javascript 设计模式系统讲解与应用 (opens new window)

# 一、导学

# 论工程师的设计能力

  • 3年工作经验,面试必考设计能力
  • 成为项目技术负责人,设计能力是必要基础
  • 从写好代码,到做好设计,设计模式是必经之路

# 前端学习设计模式的困惑

  • 网上的资料都是针对 Java 等后端语言的
  • 看懂概念,但是不知道怎么用,看完就忘
  • 现在的 JS 框架如vue、react封装的很完善,基本接触不到到底都用了哪些设计模式

# 课程概述

  • 做什么?—— 讲解JS设计模式
  • 哪些部分? —— 面向对象、设计原则、设计模式
  • 技术? —— 面向对象,UML类图,ES6

# 知识点介绍

  • 面向对象:ES class语法、三要素、UML类图
  • 设计原则:何为设计、5大设计原则、从设计到模式
  • 设计模式:分优先级讲解、结合核心技术、结合框架应用
  • 综合示例:设计方案、代码演示、设计模式对应关系的分析

# 课程安排

# 1、面向对象

  • 使用 webpack 和 babel 搭建ES6编译环境
  • ES6 class 面向对象的语法
  • 面向对象三要素:继承 封装 多态

# 2、设计原则

  • 通过 《Linux/Unix设计哲学》理解何为设计
  • 5大设计原则分析和理解,以及代码演示
  • 设计模式 -> 从设计到模式

# 3、设计模式

  • 概述:创建型、结构型、行为型
  • 常用设计模式,详细讲解,结合经典使用场景
  • 非常用设计模式,理解概念,示例演示
  • 有主有次,掌握重点

# 4、综合示例

  • 用jQuery实现一个简单的购物车
  • 设计分析,画UML类图
  • 代码演示
  • 总结使用的7种设计模式

# 讲授方式

  • 先基础后实践,先设计后模式
  • 重点、常用的设计模式,配合经典使用场景
  • 综合示例,演示设计模式如何使用
  • 用JS的方式讲解面向对象和设计模式

# 课程收获

  • 面向对象思想,UML类图
  • 5大设计原则,23种设计模式
  • 能应对前端面试中相关的面试题
  • 提升个人的设计能力

# 学习前提

  • 了解面向对象,能熟练使用jQuery或类似工具库
  • 有ES6语法基础,用过nodejs和npm环境
  • 了解vue和react(至少看过文档,做过demo)

# 重点提示

  • 本课讲解设计模式,不是实战项目也不是源码分析
  • 23种设计模式不是都常用,分清主次
  • 设计模式在JS和Java中的讲解方式有区别
  • 不适合刚入门编程的同学,参考上文的学习前提

# 二、面向对象

# 概念

  • 对象(实例)

# 三要素

  • 继承:子类继承父类
  • 封装:数据的权限和保密
  • 多态:同一接口不同实现

# 继承

作用:继承可以将公共方法抽离出来,提高复用,减少冗余

# 封装

  • public完全开放
  • protected对子类开放
  • private对自己开放

作用:减少耦合,不该外漏的不外漏,利于数据、接口的权限管理 (ES6目前不支持,可使用TS实践)

# 多态

作用:保持子类的开放性和灵活性,面相接口编程

JS使用极少,了解即可

# 为何使用面向对象?

原因:结构化的事务容易管理

  • 程序结构化:顺序、分支、循环
  • 数据结构化:面相对象

# UML类图

UML:Unified Modeling Language 统一建模语言

  • UML 包含很多的图,类图是其中一个

  • 关系、泛化、关联

# 三、设计原则

# 何为设计?

  • 描述
    • 按照哪一种思路或者标准来实现功能
    • 功能相同,可以有不同设计方案来实现
    • 伴随着需求增加,设计的作用才能体现出来
  • 《Unix/Linux设计哲学》
    • 准则1:小即是美
    • 准则2:让每个程序员只做一件事
    • 准则3:快速建立原型
    • 准则4:舍弃高效率而取可移植性
    • 准则5:采用纯文本来存储数据
    • 准则6:充分利用软件的杠杆效应(复用)
    • 准则7:使用shell脚本来提高杠杆效应和可移植性
    • 准则8:避免强制性的用户界面
    • 准则9:让每个程序都成为过滤器
    • 小准则:允许用户定制环境
    • 小准则:尽量使操作系统内核小而轻量化
    • 小准则:使用小写字母并尽量简写
    • 小准则:沉默是金(异常时不输出)
    • 小准则:各部分之和大于整体
    • 小准则:寻求90%的解决方案

# 五大设计原则

SOLID五大设计原则

  • S - 单一职责原则
  • O - 开放封闭原则
  • L - 李氏置换原则
  • I - 接口独立原则
  • D - 依赖倒置原则

# S-单一职责原则

描述:

  • 一个程序只做好一件事
  • 如果功能过于复杂就拆分开,每个部分保持独立

# O-开放封闭原则

描述:

  • 对扩展开放,对修改封闭
  • 增加需求时,扩展新代码,而非修改已有代码
  • 这是软件设计的终极目标

# L-李氏置换原则

  • 子类能覆盖父类
  • 父类能出现的地方子类就能出现
  • JS中使用较少(弱类型&继承使用较少)

# I-接口独立原则

  • 保持接口的单一独立,避免出现“胖接口”
  • JS中没有接口(TS例外),使用较少
  • 类似于单一职责原则,这里更关注接口

# D-依赖倒置原则

  • 面相接口编程,依赖于抽象而不依赖与具体
  • 使用方只关注接口而不关注具体类的实现
  • JS中使用较少(没有接口&弱类型)

# 设计原则总结

  • S O 体现较多,详细介绍
  • L I D 体现较少,但是要了解其用意

# 从设计到模式

  • 设计
  • 模式
  • 分开
  • 从设计到模式

# 23种设计模式

  • 创建型
    • 工厂模式(工厂方法模式,抽象工厂模式,建造者模式)
    • 单例模式
    • 原型模式
  • 组合型
    • 适配器模式
    • 装饰器模式
    • 代理模式
    • 外观模式
    • 桥接模式
    • 组合模式
    • 享元模式
  • 行为型
    • 策略模式
    • 模板方法模式
    • 观察者模式
    • 迭代器模式
    • 职责链模式
    • 命令模式
    • 备忘录模式
    • 状态模式(状态机)
    • 访问者模式
    • 中介者模式
    • 解释器模式

# 如何学习设计模式?

  • 明白每个设计模式的道理和用意
  • 通过经典应用体会它的真正使用场景
  • 自己编码时多思考,尽量模仿(刻意训练)

# 四、工厂模式

# 介绍

将new操作单独封装,遇到new时,就要考虑是否该使用工厂模式

# 示例

  • 购买汉堡,直接点餐、取餐,不会自己亲手做;
  • 商店要封装做汉堡的工作,做好直接给消费者

# 场景

  • jQuery,$('div'),支持链式操作,将jQuery名字封装起来
  • React.createElement 创建vnode

# 阅读源码的意义

  • 学习功能实现
  • 学习设计思路
  • 强制自己写代码,模拟刻意训练
  • 自己写出优秀的代码

# jQuery

class jQuery {
    constructor(selector) {
        
    }
}
window.$ = function(selector) {
    return new jQuery(selector)
}
1
2
3
4
5
6
7
8

# React.createElement

class Vnode(tag, attrs, children) {
    // 省略内部代码...
}
React.createElement = function(tag, attrs ,children) {
    return new Vnode(tag, attrs, children)
}
1
2
3
4
5
6

# Vue异步组件

Vue.component('async-example', function(resolve, reject) {
    setTimeout(() => {
        resolve({
            template: '<div>I am async!</div>'
        })
    }, 1000)
})
1
2
3
4
5
6
7

# 设计原则验证

  • 构造函数和创建者分离
  • 符合开放封闭原则

# 五、单例模式

# 介绍

一个类只有一个实例

# 示例

登录框、购物车

# 代码演示

Java使用单例模式

public class SingleObject {
    private SingleObject(){ // private约束不能在外部使用构造函数
    }
    private SignleObject instance = null;
    public SingleObject getInstance() {
        if (instance == null) {
            // 只new一次
            instance = new SingleObject();
        }
        return instance;
    }
    public void login(username, password) {
        System.out.println('login...')
    }
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

JS使用单例模式(靠文档注释约束)

class SingleObject {
    login() {
        console.log('login...')
    }
}
SingleObject.getInstance = (function() {
    let instance
    return function() {
        if (!instance) {
            instance = new SingleObject();
        }
        return instance
    }
})()

// 测试:注意这里只能使用静态函数 getInstance,不能 new SingleObject()!!!
let obj1 = SingleObject.getInstance()
obj1.login();
let obj2 = SingleObject.getInstance()
obj2.login();
console.log(obj1 === obj2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

# 场景

  • jQuery只有一个$
  • 模拟登录框(购物车)
  • vuex 和 redux 中的 store

# jQuery只有一个$

if (window.jQuery != null) {
    return window.jQuery
} else {
    // 初始化...
}
1
2
3
4
5

# 设计原则验证

  • 符合单一职责原则,只实例化唯一的对象

# 六、适配器模式

# 介绍

旧接口格式和使用者不兼容,中间加一个适配转换接口

# 示例

  • 插头转接口
  • client -> target(adapter)

# 场景

  • 封装旧接口
  • vue 的 computed

# 设计原则验证

  • 将旧接口和使用者进行分离
  • 符合开放封闭原则

# 七、装饰器模式

# 介绍

为对象添加新功能,不改变其原有的结构和功能

# 示例

  • 手机壳

# URL类图和代码演示

# 场景 ES7 装饰器

/**
* 装饰器的原理
**/

@decorator
class A {}

// 等同于
class A {}
A = decorator(A) || A;
1
2
3
4
5
6
7
8
9
10

# 场景 core-decorators第三方库

  • 第三方开源lib
  • 提供常用的装饰器
import { deprecate } from 'core-decorators'

class Person {
  @deprecate('即将废弃', {url: 'www.baidu.com'})
  name() {
    return 'Aaron'
  }
}

let p = new Person();
p.name();
1
2
3
4
5
6
7
8
9
10
11

# 设计原则验证

  • 将现有对象和装饰器进行分离,两者独立存在,符合开放封闭原则

# 八、代理模式

# 介绍

  • 使用者无权访问目标对象
  • 中间加代理,通过代理做授权和控制
  • 用户 - 代理对象 - 目标对象

# 示例

  • 科学上网
  • 明星经纪人

# 传统UML类图&演示

Client(proxyImg) -> ProxyImg(realImg/display) -> RealImg(fileName/display)

# 场景

  • 网页事件代理
  • jQuery $.proxy
  • ES6 Proxy

# 网页事件代理

let div1 = document.getElementById('div1')
div1.addEventListener('click', function(e) {
    var target = e.target
    if (target.nodeName === 'A') {
        alert(target.innerHTML)
    }
})
1
2
3
4
5
6
7

# $.proxy

# ES6 Proxy


// 明星
let star = {
  name: 'Aaron',
  age: 31,
  phone: 'star_1381809xxxx'
}

// 经纪人
let agent = new Proxy(star, {
  get: function(target, key) {
    if (key === 'phone') {
      // 返回经纪人自己的手机号
      return 'agent_13800000000'
    }

    if (key === 'price') {
      // 明星不保价,经纪人报价
      return '报价_120000'
    }

    return target[key]
  },
  set: function(target, key, val) {
    if (key === 'customPrice') {
      if (val < 100000) {
        throw new Error('价格太低')
      } else {
        target[key] = val
        return true
      }
    }
  }
})

console.log(agent.name)
console.log(agent.age)
console.log(agent.phone)
console.log(agent.price)

agent.customPrice = 200000
console.log('agent.customPrice', agent.customPrice)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42

# 设计原则验证

  • 代理类和目标类分离,隔离开目标类和使用者
  • 符合开放封闭原则

# 代理模式 vs 适配器模式

  • 适配器模式:提供一个不同的接口(如不同版本的插头)
  • 代理模式:提供一模一样的接口

# 代理模式 vs 装饰器模式

  • 装饰器模式:扩展功能,原有功能不变且可直接使用
  • 代理模式:显示原有功能,但是经过限制或者阉割之后的

# 示例对比

  • 代理模式 —— 经纪人
  • 适配器模式 —— 插头转换器
  • 装饰器模式 ——手机壳

# 九、外观模式

# 介绍

为子系统中的一组接口提供了一个高层接口,使用者使用这个高层接口

# 示例

  • 去医院看病,接待员去挂号、缴费、看诊、取药

# UML类图

Client -> Facade -> (SubSystemA/SubSystemB/SubSystemC)

# 十、观察者模式

# 介绍

  • 发布 & 订阅
  • 支持一对多

# 示例

  • 订报纸、订牛奶

# UML类图

# 代码演示

class Subject {
  constructor() {
    this.state = 0
    this.observers = []
  }
  getState() {
    return this.state
  }
  setState(state) {
    this.state = state
    this.notifyAllObservers()
  }
  notifyAllObservers() {
    this.observers.forEach(observer => {
      observer.update()
    })
  }
  attach(observer) {
    this.observers.push(observer)
  }
}

class Observer {
  constructor(name, subject) {
    this.name = name
    this.subject = subject
    this.subject.attach(this)
  }
  update() {
    console.log(`${this.name} update, state: ${this.subject.getState()}`)
  }
}

// 测试
let s = new Subject()
let o1 = new Observer('o1', s)
let o2 = new Observer('o2', s)
let o3 = new Observer('o3', s)
s.setState(1)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39

# 场景

  • 网页事件绑定
  • Promise(pending状态变化为fulfilled时,调用resolved处理函数)
  • nodejs自定义事件(底层eventEmitter、api如fs.createReadSteam)
  • nodejs处理http请求
  • vue react 组件生命周期触发
  • vue watch 实现

# 设计原则验证

  • 主题和观察者分离,不是主动触发而是被动监听,两者解耦
  • 符合开放封闭原则

# 十一、迭代器模式

# 介绍

  • 顺序访问一个有序集合(非对象)
  • 使用者无需知道集合的内部结构(封装)

# 示例

  • Array.prototype.forEach
  • for语句
  • $.each()

# UML类图

# 演示

class Iterator {
    constructor(container) {
        this.list = container.list
        this.index = 0
    }
    next() {
        if (this.hasNext()) {
            return this.list[this.index++]
        }
        return null
    }
    hasNext() {
        return this.index < this.list.length
    }
}

class Container {
    constructor(list) {
        this.list = list
    }
    getIterator() {
        return new Iterator(this)
    }
}

let  arr = [1,2,3,4]
let container = new Container(arr)
let iterator = container.getIterator()
while(iterator.hasNext()) {
    console.log(iterator.next())
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

# 场景

  • jQuery each
  • ES6 Iterator

# ES6 Iterator为何存在?

  • ES6 语法中,有序集合的数据类型已经有很多
  • Array Map Set String TypedArray arguments NodeList
  • 需要有一个统一的遍历接口来遍历所有数据类型
  • 注意:Object不是有序集合,可以用Map代替

# ES6 Iterator是什么

  • 以上数据类型,都有 [Symbol.iterator] 属性
  • 属性值是函数,执行函数返回一个迭代器
  • 这个迭代器就有 next 方法可顺序迭代子元素
  • 可运行 Array.prototype[Symbol.iterator] 来测试

# ES6 Iterator实例


// `Symbol.iterator` 并不是人人都知道,也不是每个人都需要封装一个each方法
// 因此有了 `for...of` 语法
function each(data) {

  // 带有遍历器特性的对象:data[Symbol.iterator] 有值
  // for (let item of data) {
  //  console.log(item)
  // }

  let iterator = data[Symbol.iterator]()
  
  // console.log(iterator.next())
  // console.log(iterator.next())

  let item = {done: false}

  while(!item.done) {
    item = iterator.next()
    if (!item.done) {
      console.log(item.value)
    }
  }
}

let arr = [1,2,3,4]
let nodeList = document.getElementsByTagName('p')
let m = new Map()
m.set('a', 100)
m.set('b', 200)

each(arr)
each(nodeList)
each(m)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34

# 设计原则验证

  • 迭代器对象和目标对象分离
  • 迭代器将使用者与目标对象隔离开
  • 符合开放封闭原则

# 十二、状态模式

# 介绍

  • 一个对象有状态变化
  • 每次状态变化都会触发一个逻辑
  • 不能总是用 if...else 来控制

# 示例

  • 交通信号灯不同颜色的变化

# UML类图

State <-- Context

# 场景

  • 有限状态机
  • promise

# 十三、其他设计模式

# 原型模式

  • 介绍:clone自己
  • 场景:Object.create

# 桥接模式

  • 介绍:用于把抽象化与实现化解耦,使得二者能够独立变化
  • 场景:Canvas绘图

# 面试相关(重要)

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

设计模式:

  • 设计 => 模式
  • 5大设计原则 SOLID
    • Single 单一职责原则
    • Open-Close 开放封闭原则
    • Lee 里氏替换原则
    • Interface 接口独立原则
    • Dependence 依赖倒置原则
  • 23个设计模式
    • 创建型
    • 组合型
    • 行为型
  • 前端常见设计模式
    • 工厂模式
      • jQuery,$('div'),支持链式操作,将jQuery名字封装起来
      • React.createElement 创建vnode
    • 单例模式
      • jQuery只有一个$
      • 模拟登录框(购物车)
      • vuex 和 redux 中的 store
    • 原型模式
      • JS 的 Object.create
    • 适配器模式
      • vue 的 computed
    • 代理模式
      • ES6 Proxy
    • 观察者模式
      • 网页事件绑定
      • Promise(pending状态变化为fulfilled时,调用resolved处理函数)(状态模式)
      • vue watch 实现
    • 迭代器模式
      • ES6 Iterator([Symbol.iterator] 属性)
      • ES6 Iterator为何存在?
        • ES6 语法中,有序集合的数据类型已经有很多
        • Array Map Set String TypedArray arguments NodeList
        • 需要有一个统一的遍历接口来遍历所有数据类型
        • 注意:Object不是有序集合,可以用Map代替
      • ES6 Iterator是什么
        • 以上数据类型,都有 [Symbol.iterator] 属性
        • 属性值是函数,执行函数返回一个迭代器
        • 这个迭代器就有 next 方法可顺序迭代子元素
        • 可运行 Array.prototype[Symbol.iterator] 来测试
    • 观察者模式和发布订阅模式的区别
      • 观察者模式中,Subject 和 Observer直接绑定,没有中间媒介,如 addEventListener 绑定事件
      • 发布订阅模式中,Publisher 和 Observer 互不相识,需要中间媒介 Event channel,如 EventBus 自定义事件

# 日常使用

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

# 参考资料

上次更新: 3/26/2022, 2:51:46 PM