name | title | tags | categories | info | time | desc | keywords | ||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
《JavaScript设计模式与开发实践》学习笔记(十) |
《JavaScript设计模式与开发实践》学习笔记(十) |
|
学习笔记 |
第二部分、第 15 章 装饰者模式、第 16 章 状态模式 |
2019/10/2 |
JavaScript设计模式与开发实践, 资料下载, 学习笔记 |
|
在传统的面向对象语言中,给对象添加功能常常使用继承的方式,但是继承的方式并不灵活, 还会带来许多问题:一方面会导致超类和子类之间存在强耦合性,当超类改变时,子类也会随之 改变;另一方面,继承这种功能复用方式通常被称为“白箱复用”,“白箱”是相对可见性而言的, 在继承方式中,超类的内部细节是对子类可见的,继承常常被认为破坏了封装性。
使用继承还会带来另外一个问题,在完成一些功能复用的同时,有可能创建出大量的子类, 使子类的数量呈爆炸性增长。
装饰器模式是指在不改变对象自身的基础上,在程序运行期间给对象动态地添加职责。
class Plane {
fire () {
console.log('发射普通子弹')
}
}
class MissileDecorator {
constructor (plane) {
this.plane = plane
}
fire () {
this.plane.fire()
console.log('发射导弹')
}
}
var plane = new Plane()
plane = new MissileDecorator(plane)
plane.fire()
// 发射普通子弹
// 发射导弹
上面的例子中,导弹修饰器就像一个可拆卸装备一样,可以装饰在飞机这个类上,丰富了飞机的开火功能。
JavaScript 语言动态改变对象相当容易,我们可以直接改写对象或者对象的某个方法,并不 需要使用“类”来实现装饰者模式。此外 ES7 还定义了属于 JavaScript 的装饰器实现方法,也可以使用这个官方的语法来实现装饰者模式。
var plane = {
fire: function () {
console.log('发射普通子弹')
}
}
var missileDecorator = function () {
console.log('发射导弹')
}
var fire1 = plane.fire
plane.fire = function () {
fire1()
missileDecorator()
}
plane.fire()
// ES7 decorator实现
// 类装饰
@missileDecorator
class Plane {
fire () {
console.log('发射普通子弹')
}
}
function missileDecorator (target) {
let fire1 = target.prototype.fire
target.prototype.fire = function () {
fire1.apply(this, arguments)
console.log('发射导弹')
}
}
let plane = new Plane()
plane.fire()
// 发射普通子弹
// 发射导弹
// ES7 decorator实现
// 方法装饰
class Plane {
@missileDecorator
fire () {
console.log('发射普通子弹')
}
}
function missileDecorator (target, name, descriptor) {
console.log('发射导弹')
}
// 发射导弹
// 发射普通子弹
装饰者模式和第 6 章的代理模式的结构看起来非常像,这两种模式都描述了怎么样为对象提供一定程度上的间接引用,并且向那个对象发送请求。
这两者最重要的区别在于它们的设计意图和设计目的:
- 代理模式的目的是,当不方便或者无需直接访问本体时,为这个本体提供一个替代者。本体用于定义功能,而代理者用于做一些准入或者验证的功能,这种关系是在一开始就确定了的。
- 而装饰器模式则用于一开始不能准确定义对象的全部功能,需要通过层层装饰链来添加功能的场景,本体只实现了部分的核心功能,而装饰器用于实现更多的功能。
状态模式的关键是区分事物内部的状态,事物内部状态的改变往往会带来事物的行为改变。
一个类在不同的状态下可能会有不同的表现形式,在通常情况下我们会在类中定义一个状态符号用于区分其表现形式。同时在一些行为中通过 if-else 逻辑进行行为区分。
class Light {
state = 'off' // 电灯初始状态
lightPress () {
if (this.state == 'off') {
this.state = 'on'
console.log('开灯')
} else if (this.state == 'on') {
this.state = 'off'
console.log('关灯')
}
}
}
上面完成了一个基本的状态机,而这段代码有以下几个缺点:
- 违反开放封闭原则,电灯开关每增加一种状态,lightPress 函数就要改动一次代码
- 所有跟状态有关的行为都被封装在了 lightPress 方法中,在上面的例子中表现为改变开关状态(this.state = 'on')和打印该行为(console.log('开灯')),非常不利于职责的切分
- 状态的切换非常不明显,仅仅表现为对 state 变量赋值,也无法一眼就分辨出到底有多少种状态(当然你可以选择在旁边加注释 - -)
- 状态之间的切换不过是堆砌 if else 语句,增加一个状态可能需要更改若干个操作,维护成本太高
状态模式的关键是把事物的 每种状态都封装成单独的类,跟此种状态有关的行为都被封装在这个类的内部。
同时我们还可以把状态的切换规则事先分布在状态类中, 这样就有效地消除了原本存在的大量条件分支语句。
class OffLight {
constructor (light) {
this.light = light
}
lightPress () {
this.light.currState = this.light.onLight
console.log('开灯')
}
}
class OnLight {
constructor (light) {
this.light = light
}
lightPress () {
this.light.currState = this.light.offLight
console.log('关灯')
}
}
class Light {
onLight = new OnLight(this)
offLight = new OffLight(this)
// 默认状态为关
currState = this.offLight
lightPress () {
this.currState.lightPress()
}
}
var a = new Light()
a.lightPress()
// 开灯
a.lightPress()
// 关灯
如此一来,我们不再使用一个字符串来记录当前的状态,而是使用更加立体化的状态对象,这样一来,我们就能很明显的看到电灯一共有几种状态。
而此时的 lightPress 函数也不会进行任何实质性的操作,而是将请求委托给当前持有的状态对象去执行。
状态的切换规律被事先定义在各个状态类中,不需要通过大量的 if-else 来进行条件切换。
事实上,状态模式在 Gof 中的定义为:
允许一个对象在其内部状态改变时改变它的行为,对象看起来似乎修改了它的类。
在上面的例子中,Light 类在状态模式中被称为 Context(上下文),Context 会持有所有状态对象的引用,以便把请求委托给状态对象。
由于 JavaScript 并没有一些严格的约束行为,如接口和抽象类(TypeScript 就有),所以很难保证所有的子类都会实现同样的方法,也就难以保证在使用状态模式时不会出错。
所以我们能做的,就是给所有的状态类一个父类,让程序在运行时抛错,虽然没有在编译时就抛错来得优雅,但总好过犯错导致关键失误:
class State {
lightPress () {
throw new Error('状态子类必须拥有lightPress方法!')
}
}
class OffLight extends State {
constructor (light) {
super()
this.light = light
}
lightPress () {
this.light.currState = this.light.onLight
console.log('开灯')
}
}
class OnLight extends State {
constructor (light) {
super()
this.light = light
}
lightPress () {
this.light.currState = this.light.offLight
console.log('关灯')
}
}
优点:
- 定义了状态与行为之间的联系,并将它们封装在了一个类里面。通过增加新的状态类,就可以增加新的状态和转换。
- 避免了 Context 的无限膨胀,去掉了 Context 过多的条件分支
- 使用对象来代替字符串记录当前状态,使得状态的切换更加一目了然
- Context 中的请求动作和状态类中封装的行为可以非常容易地独立变化而不受影响
缺点:
- 会导致定义过多的状态类,系统中会添加不少对象
- 无法一眼看出具体的不同状态间的转换逻辑,只能从各个状态类中挨个查看
一些持续的状态类可以共享,而无需为每一个 Context 都重新生成,从而节省执行时间
状态模式和策略模式的共同点是都有一个上下文,一些策略类或者状态类,上下文把请求委托给这些类来执行。
但他们是两种完全不同的设计模式,其中的区别在于:
- 策略模式中的各个策略类之间是平等又平行的,它们之间没有任何联系。使用者必须熟知这些策略的作用,才能主动地切换算法
- 状态模式中的状态和对应行为是已经被封装好的,其切换规则也已经被规定完成,对于使用者而言完全无需了解状态类的执行细节。
像以前介绍的模式一样,JavaScript 灵活的特性决定了其不需要根据传统语言的编写方式来进行状态类的编写。
var FSM = {
offLight: {
lightPress: function () {
console.log('开灯')
this.currState = this.onState
}
},
onLight: {
lightPress: function () {
console.log('关灯')
this.currState = this.offState
}
}
}
class Light {
onState = FSM.onLight
offState = FSM.offLight
// 默认状态为关
currState = this.offState
lightPress () {
this.currState.lightPress.apply(this, arguments)
}
}
上面的代码要注意的是 this 指针的引用,为确保状态类中的 this 可以指向当前实例,需要使用 apply 或者 call 改变方法内的指针指向。