首先介绍了两个较为容易理解的设计模式:Iterator 迭代器模式和 Adapter 适配器模式。
- 迭代器模式:隐藏实现的设计模式。
- 适配器模式:包装接口,将现有的接口包装为期望的接口。
Iterator 迭代器模式
定义
迭代器是专用于迭代的一个对象,可以让用户透过特定的接口巡访容器中的每一个元素而不用了解底层的实现,分离了遍历和实现。
角色
- 集合接口类:定义了集合类必须有创建迭代器的接口。
- 迭代器接口类:定义了迭代器对象具体接口,由实现角色实现其定义的接口。
- 集合实现类:实现具体迭代器,实现时会调用
具体的迭代器实现类
。 - 迭代器实现类:
迭代器接口类
的实现角色,实现迭代器接口类
定义的所有接口,由于其需要对集合容器中的元素进行操作,所以。迭代器实现类
对集合实现类
应该是知晓的,在后续的代码中我们可会看到
JavaScript 实现
在 JavaScript 中能规定接口的方式有很多种,如约定俗称(注释)、单元测试、TypeScript 等,我们使用基于 Class 弱约束进行代码示例。
// * 集合接口类
class Aggregate {
iterator () {
throw new Error('需实现 Aggregate 类的 iterator 方法')
}
}
// * 迭代器接口类
class Iterator {
hasNext () {
throw new Error('需实现 Iterator 类的 hasNext 方法')
}
next () {
throw new Error('需实现 Iterator 类的 next 方法')
}
}
// * 集合实现类
class LikeArray extends Aggregate {
constructor(list) {
super()
this.list = list
}
getByIndex (index) {
return this.list[index]
}
getLength () {
return this.list.length
}
iterator () {
return new ReverseIterator(this)
}
}
// * 迭代器实现类
class ReverseIterator extends Iterator {
constructor (array) {
super()
this.array = array
this.index = this.array.getLength()
}
hasNext () {
if (this.index >= 0) {
return true
} else {
return false
}
}
next () {
const item = this.array.getByIndex(this.index)
this.index -= 1
return item
}
}
// * 使用
const likeArray = new LikeArray([5,4,3,2,1])
const iterator = likeArray.iterator()
while(iterator.hasNext()) {
console.log(iterator.next())
}
// 1 2 3 4 5
拓展
在 JavaScript 中有迭代器协议
,类似于接口类,它规定了我们可以通过实现对象的 [Symbol.iterator]
方法,返回一个包含 next
方法的迭代器对象的方式来完成或改写一个对象的迭代行为(for...of
),如实现 Object 或 Function 型对象的[Symbol.iterator]
方法使其可迭代,其中 next
方法的返回值是一个包含 done
(表示是否不可继续的布尔值,类似于示例代码的 hasNext 方法返回值),value
(当前次的值,类似于示例代码中的 next 方法返回值)的对象。
- 实现函数迭代中按顺序返回函数名
function demo () {} for (let value of demo) { console.log(value) } //TypeError: demo is not iterable demo[Symbol.iterator] = function () { return { next () { if (this.index < this.name.length) { this.index += 1 return { value: this.name[this.index - 1], done: false } } else { return { done: true } } }, index: 0, name: this.name } } for (let value of demo) { console.log(value) } // d e m o
- 实现对象迭代中按值大小返回键名
const test = {a: 3, e: 1, f: 2} for (let value of test) { console.log(value) } //TypeError: demo is not iterable test[Symbol.iterator] = function () { let list = Object.entries(test).sort((pre, next) => pre[1] - next[1]) return { next () { if (this.index < list.length) { this.index += 1 return { value: list[this.index - 1][0], done: false } } else { return { done: true } } }, index: 0 } } for (let value of test) { console.log(value) } // e f a
- 改写数组迭代为反序迭代
const array = [1,2,3,4] for (let value of array) { console.log(value) } // 1 2 3 4 array[Symbol.iterator] = function () { const initalLength = this.length - 1 let array = this return { next () { if (this.index >= 0) { this.index -= 1 return { value: array[this.index + 1], done: false } } else { return { done: true } } }, index: initalLength } } for (let value of array) { console.log(value, '??') } // 4 3 2 1
可以看出使用 for...of
时也是对可迭代对象的内部细节是一无所知的。其实我们还可以对基本的 Function、Object、Array 型对象的取值等方式进行封装,使其内部实现对迭代器不可见,仅供迭代器调用接口,正如 JavaScript 实现部分提供 getLength
、getByIndex
等方法一样,同样实现隔离,那么我们就可以再次抽取逻辑,使用一套代码对不同的对象实现相同的迭代模式。
思考
迭代器模式的重点不局限于迭代器本身的实现,在于该如何合理的分离功能和实现。我们需要修改具体实现时,是否可以仅限于实现部分,而不需要通知所有的使用方。
如提供给用户的数据 data 内部实现由 Array 改为 Object 时,仅仅需要调整提供给用户的 next、hasNext(也属于 data 的部分)方法提供同样的功能,而不需要用户修改他的代码,又如仅需调整内部实现提供给迭代器的 getLength、getByIndex 方法提供同样的功能,而不需要对迭代器修改。
从一个更好理解的角度讲,如 api 接口,数据库的数据结构变了,只要接口的返回值不变,那么接口适用方就不需要做任何修改。
当然这种屏蔽实现提供接口的方式,会减弱使用者的自由度,要求我们必须更好的知道使用方的使用意图,才能兼顾可维护性和开发成本。
Adapter 适配器模式
定义
将“现有程序”转化为满足使用需求的程序的设计模式就是 Adapter 模式。适配器模式也可称为包装器模式。
书中对适配器模式的实现进行了分类:
- 类适配器模式
- 对象适配器模式
角色
- 目标:即满足使用需求的程序。
- 使用者:程序需求方,定义了目标的接口,目标的直接使用者,一般是实际的业务代码。
- 被适配者:现有程序。
- 适配器:负责将被适配者转化为目标。
Javascript 实现
先假定现有的程序为一个 dom 操作类。
class Jquery {
constructor (selector) {
this.dom = xxx(selector)
}
css (name, value) {
console.log(this.dom, name, value)
this.dom.style[name] = value
}
}
而需要的只是一个提供显示和隐藏的 dom 操作类,即提供了如下方法的类。
class Interface {
hide () {}
show () {}
}
类适配器模式
由于 JavaScript 中不能实现继承父类的同时实现接口,所以我们仅比较僵硬的实现一下。
class Target extends Jquery {
constructor (selector) {
super(selector)
}
hide () {
super.css('display', 'none')
}
show () {
super.css('display', 'block')
}
}
对象适配器模式
class Target {
constructor (selector) {
this.dom = new Jquery(selector)
}
hide () {
this.dom.css('display', 'none')
}
show () {
this.dom.css('display', 'block')
}
}
不同
类适配器模式和对象适配器模式结构上最大的不同是前者直接继承现有类,将功能的调用交给父类,对象适配器模式则是实例化现有类,并将功能的调用交给自己。
同时,类适配器模式由于连续继承比较麻烦,不如对象适配器模式那么容易将多个“现有程序”的功能适配在一起,比如:
class Target {
constructor (selector) {
this.dom = new Jquery(selector)
this.utils = new Utils()
}
hide () {
this.utils.xxx()
this.dom.css('display', 'none')
}
show () {
this.utils.yyy()
this.dom.css('display', 'block')
}
}
思考
在实际开发中,不固化在适配器模式的标准形式(基于类和接口)中,我们也经常会用到适配器模式的思想。
比如基于第三方库(现有),将无穷的方法包装成我们需要的更具体有限方法的工具库(目标),供业务使用。
比如跨团队的协作中,双方各定义自己的接口,同时开发,最后再由中间层抹平差异。
甚至可以反过来,在已经运行的业务代码中,需要换掉老旧的基础库时,不愿意去业务代码的汪洋大海中逐个修改,就可以添加适配层,将还在开发的新的基础库适配为原有的调用方式。
还有一点比较重要的应用点是,当接入第三方代码时,应控制好代码边界,可使用适配器模式,将对第三方代码的使用收束在可控的(引用,影响)范围内,以便将来升级或更换。