Jacleklm's Blog

设计模式

2019/10/26

PS:一般单例模式和观察者模式问的最多。一般能说出5个设计模式就可以

面向对象的JS

语言类型

静态类型语言在编译时便确定变量的类型,而动态类型语言要到程序运行的时候,待变量被赋予某个值之后,才会有某种类型。JS是典型的动态类型语言。同时JS也是弱类型语言,Java是强类型语言
强类型语言是一旦变量的类型被确定,就不能转化的语言。实际上所谓的貌似转化,都是通过中间变量来达到,原本的变量的类型肯定是没有变化的
弱类型语言则反之,一个变量的类型是由其应用上下文确定的。比如语言直接支持字符串和整数可以直接用 + 号搞定。当然,在支持运算符重载的强类型语言中也能通过外部实现的方式在形式上做到这一点,不过这个是完全不一样的内涵
通常的说,java/python都算是强类型的,而VB/Perl/C都是弱类型的

面向对象三要素

继承

ES6实现继承很方便了,不解释

封装

数据的权限和保密(eg. 变量 or 对象)
封装的三个访问权限关键字:public 完全开放,protected 对子类开放,private 对自己开放。对三个关键字可以对是属性的一种描述。
但JS没有着这种关键字(有些语言有,eg. TypeScript,Java),只能通过作用域模拟出public ,private 两种封装性。
封装的作用

  • 减少耦合,不该外露的不外露
  • 利于数据、接口的权限管理
  • ES6目前不支持,一般认为 _ 开头的属性是private

多态

同一操作作用于不同的对象上,可以产生不同的解释和不同的执行结果。(eg. 继承自同一父后又自己写方法,这是两个继承自同一父的两个子类的同个方法可能执行结果不同)
作用:保持子类的开放性和灵活性;面向接口编程

为何使用面向对象

  • 因为计算机的程序执行:顺序、判断、循环 ——实现了结构化;
  • 而使用面向对象编程能——数据结构化;
  • 对于计算机,结构化的才是最简单的,所以编程应该 简单&抽象。

PS:jQuery其实是一个类

UML类图

画UML类图帮助理解设计模式,也能帮助面对对象编程。可以去processon

设计

何为设计

按照哪一种思路或者标准来实现功能。功能相同,可以有不同的设计方案实现。伴随着需求增加,设计的作用才能体现出来

《UNIX/LINUX设计哲学》

  • 准则1:小即是美
  • 准则2:让每个程序制作好一件事
  • 准则3:快速建立原型(满足最基本需求)
  • 准则4:舍弃高效率而取可移植性(可通用性)
  • 准则5:采用纯文本来存储数据
  • 准则6:充分利用软件的杠杆效应(软件复用)
  • 准则7:使用shell脚本来提高杠杆效应和可移植性
  • 准则8:避免强制性的用户界面
  • 准则9:让每个程序成为过滤器
  • 小准则
    • 允许用户定制环境
    • 尽量使操作系统内核小而轻量化
    • 使用小写字母并尽量简短
    • 沉默是金
    • 各部分之和大于整体
    • 寻求90%的解决方案

SOLID五大设计原则

  • S:单一职责原则。每个程序(或函数/函数的一部分)只做好一件事,如果功能过于复杂就拆分,每个部分保持独立
  • O:开放封闭原则。对扩展开放,对修改封闭;增加需求时,扩展新代码,而非修改原来的代码。是软件设计的终极目标。
  • L:*李氏置换原则。子类能覆盖父类;父类能出现的地方子类都能出现;JS用的少(弱类型&继承使用较少)
  • I:*接口独立原则。保持接口的单一独立,避免出现“胖接口”;JS中没有接口(typescript例外),使用较少
  • D:*依赖导致原则。面向接口编程,依赖于抽象而不依赖于具体;使用方只关注接口而不关注具体类的实现。JS使用少

设计模式简介

  • 创建型
    • 工厂模式
    • 单例模式
    • 原型模式
  • 结构性
    • 适配器模式
    • 装饰器模式
    • 代理模式
    • 外观模式
    • *桥接模式
    • *组合模式
    • *享元模式
  • 行为型
    • *策略模式
    • *模板方法模式
    • 观察者模式(发布-订阅)
    • 迭代器模式
    • *职责连模式
    • *命令模式
    • *备忘录模式
    • 状态模式
    • *访问者模式
    • *中介者模式
    • *解释器模式

学习目的:

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

例题1

打车时,可以打专车(每公里2元)或快车(1元)。任何车都有车牌号和名称。行程开始时,显示车辆信息;行程结束时,显示打车金额(假定行程5公里)。要求画出UML类图和用ES6语法写出该示例。先画UML类图再写代码

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
class Car {
constructor(num, name) {
this.num = num
this.name = name
}
}
class FastCar extends Car {
constructor(num, name) {
super(num, name)
this.price = 1
}
}
class SpecialCar extends Car {
constructor(num, name) {
super(num, name)
this.price = 2
}
}
class Trip {
constructor(car, long) {
this.car = car
this.long = long
}
start() {
console.log(`行程开始,名称:${this.car.name},车牌号:${this.car.num}`)
}
end() {
console.log(`行程结束,价格:${this.car.price * this.long}`)
}
}

let car = new SpecialCar(100, '广州专车')
let trip = new Trip(car, 5)
trip.start()
trip.end()

例题2

某停车场,分为3层,每层100个车位,;每个车位都能监控到车辆的驶入和离开。车辆进入前,显示每层的空余车辆数;车辆进入时,摄像头可识别车牌号和时间;车辆出来时,出口显示器显示车牌号和停车时长。画UML类图

工厂模式

介绍

将new操作单独封装(初始化实例);遇到new时,就要考虑是否用工厂模式
eg. 我们去饭店点了个一个菜,后面厨师会做实例化这个菜的各种复杂操作,但是提供给我们的却只有“点菜”这个接口

作用

隐藏了创建实例的复杂度,只需要提供一个借口,简单清晰

UML类图及实现代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Product {
constructor(name) {
this.name = name
}
init() {
alert('init')
}
fn1() {
alert('fn1')
}
}
class Creator {
create(name) {
return new Product(name)
}
}
let creator = new Creator()
let p = creator.create('p1')
p.init()

场景

  • React.createElement
  • vue异步组件的创建
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    export function createComponent (
    Ctor: Class<Component> | Function | Object | void,
    data: ?VNodeData,
    context: Component,
    children: ?Array<VNode>,
    tag?: string
    ): VNode | Array<VNode> | void {

    // 逻辑处理...

    const vnode = new VNode(
    `vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
    data, undefined, undefined, undefined, context,
    { Ctor, propsData, listeners, tag, children },
    asyncFactory
    )

    return vnode
    }

    在上述代码中,我们可以看到我们只需要调用 createComponent 传入参数就能创建一个组件实例,但是创建这个实例是很复杂的一个过程,工厂帮助我们隐藏了这个复杂的过程,只需要一句代码调用就能实现功能

单例模式

介绍

系统中被唯一使用,一个类只有一个实例
eg. 淘宝页面中,登录框、购物车只会有一个;全局缓存,全局状态管理等这些只需要一个对象,就可以使用单例函数

实现代码

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
/*说明:单例模式要用到pravite关键字,但JS没有。我们只需要用一个变量确保实例只创建一次就行
*/
class SingleObject {
constructor() { }
}
SingleObject.getInstance = (function () {
let instnce
return function () {
if (!instnce) {
instnce = new SingleObject
}
return instnce
}
})()
// 下面这种把 instance 写成类的静态属性的方式也可以,更和TS中的写法一致
// 有些库的单例写法会把 instane 挂在 global 中 ,global.instance = xxx。当然这种写法建议用一个复杂一点的变量名,eg: CG_I18N_EMITER
SingleObject.instnce = undefined
SingleObject.getInstance = function () {
if (!this.instnce) {
this.instnce = new SingleObject()
}
return this.instnce
}
// 一定要用自定义的getInstance方法来创建实例,不能用new
let obj1 = SingleObject.getInstance()
let obj2 = SingleObject.getInstance()
console.log(obj1 === obj2) // true
// 用了new
let obj3 = new SingleObject()
console.log(obj1 === obj3) // false

场景

  • jQuery只有一个$
  • 模拟登录框
  • Vuex源码,通过一个外部变量来控制只安装一次 Vuex
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    let Vue // bind on install

    export function install (_Vue) {
    if (Vue && _Vue === Vue) {
    // 如果发现 Vue 有值,就不重新创建实例了
    return
    }
    Vue = _Vue
    applyMixin(Vue)
    }

适配器模式

介绍

旧接口格式和使用者不兼容,中间加一个适配转换接口
eg. 电源插口转换器;

UML类图及实现代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Adaptee {
specificRequest() {
return '德国标准插头'
}
}
class Target {
constructor() {
this.adaptee = new Adaptee()
}
request() {
let info = this.adaptee.specificRequest()
return `${info}的转换器——中国标准插头`
}
}

let target = new Target()
let res = target.request()
console.log(res) // 德国标准插头的转换器——中国标准插头

场景

  • 封装旧接口
  • Vue的计算属性
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    <body>
    <div id="app">信息列表</div>
    <p>信息:{{message}}</p>
    <p>逆序信息:{{newMessage}}</p>
    <script>
    var vm = new Vue({
    el: '#app',
    data: {
    message: 'hello'
    },
    computed: {
    newMessage: function () {
    return this.message.split('').reverse().join('')
    }
    }
    })
    </script>
    </body>

装饰器模式

介绍

不需要改变对象原有结构和功能(接口)的前提下,为对象添加新功能
eg. 就像我们经常需要给手机戴个保护套防摔一样,不改变手机自身,给手机添加了保护套提供防摔功能

UML类图及实现代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Circle {
draw() {
console.log('画一个圆形')
}
}
class Decorator {
constructor(circle) {
this.circle = circle
}
draw() {
this.circle.draw()
this.setRedBorder(circle) // 这里要传递“原来的功能”为参数,记得是“装饰”
}
setRedBorder(circle) {
console.log('设置红色边框')
}
}

let circle = new Circle()
circle.draw() // 画一个圆形
let dec = new Decorator(circle)
dec.draw() // 画一个圆形 // 设置红色边框

场景

ES7装饰器语法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@testDec // 用这个@的语法对下面的类做装饰
class Demo {
// ...
}
function testDec(target) {
target.isDec = true
}
console.log(Demo.isDec) // true

// 传递参数的情况
function testDec(isDec) {
return function(target) {
target.isDec = true
}
}
@testDec
class Demo {
// ...
}
console.log(Demo.isDec) // true

代理模式

介绍

使用者无权访问目标对象,可以通过中间加代理的方法,通过代理做授权和控制
eg. 代购;明星和经纪人;事件代理

UML类图及实现代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class RealImg {
constructor(filename) {
this.filename = filename
}
display() {
console.log(`展示${this.filename}图片`)
}
}
class ProxyImg {
constructor(filename) {
this.realimg = new RealImg(filename)
}
display() {
this.realimg.display()
}
}

let proxyImg = new ProxyImg('1.png')
proxyImg.display() // 展示1.png图片

代理模式 vs 装饰器模式

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

场景

  • ES6 proxy
  • 网页事件代理
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    <ul id="ul">
    <li>1</li>
    <li>2</li>
    <li>3</li>
    <li>4</li>
    <li>5</li>
    </ul>
    <script>
    let ul = document.querySelector('#ul')
    ul.addEventListener('click', (event) => {
    console.log(event.target);
    })
    </script>

    因为存在太多的 li,不可能每个都去绑定事件。这时候可以通过给父节点绑定一个事件,让父节点作为代理去拿到真实点击的节点

观察者模式

介绍

通过一对一或者一对多的依赖关系,当对象发生改变时,订阅方都会收到通知,即 发布 & 订阅(发布给订阅者)
eg. 肯德基很多个人点餐完成,每准备好一个顾客的餐那个顾客就会收到通知;双11某商品降价通知

UML类图及实现代码

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
// 主题,保存状态,状态变化之后触发所有观察者对象
class Subject {
constructor(name) {
this.state = 0
this.observers = []
this.name = name
}
// 添加观察者
addOvservers(observer) {
this.observers.push(observer)
}
getState() {
return this.state
}
setState(state) {
this.state = state
this.notifyAllObservers()
}
notifyAllObservers() {
this.observers.forEach((observer) => {
observer.update()
})
}
}
// 观察者
class Observer {
constructor(name, subject) {
this.name = name
this.subject = subject
this.subject.addOvservers(this)
}
update() {
console.log(`${this.subject.name} updata, now state is ${this.subject.getState()}`)
}
}
let s = new Subject('subject1')
let o1 = new Observer('o1', s)
let o2 = new Observer('o2', s)
s.setState(2) // subject1 updata, now state is 2 // subject1 updata, now state is 2

场景

  • 网页的addEventListener
  • Vue的双向数据绑定、父子传值$emit、生命周期触发、Vue的watch
  • Promise的 .then(监听promise的状态变化)
  • Node.js自定义事件EventEmitter(这个其实和Vue的$emit是一样的),http(其实底也是EventEmitter)

外观模式

介绍

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

UML类图

场景

实现一个兼容多种浏览器的添加事件方法

1
2
3
4
5
6
7
8
9
10
11
function addEvent(elm, evType, fn, useCapture) {
if (elm.addEventListener) {
elm.addEventListener(evType, fn, useCapture)
return true
} else if (elm.attachEvent) {
var r = elm.attachEvent("on" + evType, fn)
return r
} else {
elm["on" + evType] = fn
}
}

对于不同的浏览器,添加事件的方式可能会存在兼容问题。如果每次都需要去这样写一遍的话肯定是不能接受的,所以我们将这些判断逻辑统一封装在一个接口中,外部需要添加事件只需要调用 addEvent 即可

迭代器模式

介绍

顺序遍历有序集合(Array,NodeList,Set等,对象不算,不是有序),使用者无需知道集合的内部结构(被封装了,不知道它是数组还是NodeList还是啥的,也不知道它的长度)。(所以 Array.prototype.forEach(), for循环 这些都不算迭代模式)

UML类图及实现代码

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
class Iterator {
constructor(conatiner) {
this.list = conatiner.list
this.index = 0
}
next() {
if (this.hasNext()) {
return this.list[this.index++]
}
return null
}
hasNext() {
if (this.index >= this.list.length) {
return false
}
return true
}
}

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

// 测试代码
let container = new Container([1, 2, 3, 4, 5])
let iterator = container.getIterator()
while (iterator.hasNext()) {
console.log(iterator.next())
}

场景

  • jQuery each
  • ES6 Iterator

状态模式

介绍

一个对象有状态变化,每次状态变化都会触发一个逻辑,当状态有很多种的时候不能总是用 if…else 来控制
eg. 交通信号灯不同颜色的变化

UML类图及实现代码

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
// 状态(红灯、绿灯、黄灯)
class State {
constructor(color) {
this.color = color
}
handle(context) {
console.log(`turn to ${this.color} light`)
context.setState(this)
}
}
// 主体
class Context {
constructor() {
this.state = null
}
getState() {
return this.state
}
setState(state) {
this.state = state
}
}

let context = new Context()
let green = new State('green')
let yellow = new State('yellow')
let red = new State('red')
// 绿灯亮了
green.handle(context)
console.log(context.getState()) // 打印状态
// 黄灯亮了
yellow.handle(context)
console.log(context.getState()) // 打印状态

场景

  • 有限状态机
  • 写一个简单的Promise

一些很不错的博客贴 && 小测

如何消除层出不穷的 switch…case ———— 开放封闭原则

核心稳定、易扩展——开放关闭原则
备用链接

React设计模式与最佳实践

比较有历史的一本小册

参考资料
慕课网-Javascript 设计模式系统讲解与应用
poetries
《JavaScript设计模式与开发实践》

CATALOG
  1. 1. 面向对象的JS
    1. 1.1. 语言类型
    2. 1.2. 面向对象三要素
      1. 1.2.1. 继承
      2. 1.2.2. 封装
      3. 1.2.3. 多态
    3. 1.3. 为何使用面向对象
    4. 1.4. UML类图
  2. 2. 设计
    1. 2.1. 何为设计
    2. 2.2. 《UNIX/LINUX设计哲学》
    3. 2.3. SOLID五大设计原则
    4. 2.4. 设计模式简介
    5. 2.5. 例题1
    6. 2.6. 例题2
  3. 3. 工厂模式
    1. 3.1. 介绍
    2. 3.2. 作用
    3. 3.3. UML类图及实现代码
    4. 3.4. 场景
  4. 4. 单例模式
    1. 4.1. 介绍
    2. 4.2. 实现代码
    3. 4.3. 场景
  5. 5. 适配器模式
    1. 5.1. 介绍
    2. 5.2. UML类图及实现代码
    3. 5.3. 场景
  6. 6. 装饰器模式
    1. 6.1. 介绍
    2. 6.2. UML类图及实现代码
    3. 6.3. 场景
  7. 7. 代理模式
    1. 7.1. 介绍
    2. 7.2. UML类图及实现代码
    3. 7.3. 代理模式 vs 装饰器模式
    4. 7.4. 场景
  8. 8. 观察者模式
    1. 8.1. 介绍
    2. 8.2. UML类图及实现代码
    3. 8.3. 场景
  9. 9. 外观模式
    1. 9.1. 介绍
    2. 9.2. UML类图
    3. 9.3. 场景
  10. 10. 迭代器模式
    1. 10.1. 介绍
    2. 10.2. UML类图及实现代码
    3. 10.3. 场景
  11. 11. 状态模式
    1. 11.1. 介绍
    2. 11.2. UML类图及实现代码
    3. 11.3. 场景
  12. 12. 一些很不错的博客贴 && 小测
    1. 12.1. 如何消除层出不穷的 switch…case ———— 开放封闭原则
    2. 12.2. React设计模式与最佳实践