JavaScript「结构型」设计模式

结构型模式:解决怎样组装现有对象,设计交互方式,从而达到实现一定的功能目的。例如,以封装为目的的适配器和桥接,以扩展性为目的的代理、装饰器

享元模式

享元模式:运用共享技术来减少创建对象的数量,从而减少内存占用、提高性能。

什么是“享元模式”?

享元模式:运用共享技术来减少创建对象的数量,从而减少内存占用、提高性能。

  1. 享元模式提醒我们将一个对象的属性划分为内部和外部状态
    • 内部状态:可以被对象集合共享,通常不会改变
    • 外部状态:根据应用场景经常改变
  2. 享元模式是利用时间换取空间的优化模式。

应用场景

享元模式虽然名字听起来比较高深,但是实际使用非常容易:只要是需要大量创建重复的类的代码块,均可以使用享元模式抽离内部/外部状态,减少重复类的创建。

为了显示它的强大,下面的代码是简单地实现了大家耳熟能详的“对象池”,以彰显这种设计模式的魅力。

代码实现

这里利用pythonjavascript实现了一个“通用对象池”类–ObjectPool。这个类管理一个装载空闲对象的数组,如果外部需要一个对象,直接从对象池中获取,而不是通过 new 操作

对象池可以大量减少重复创建相同的对象,从而节省了系统内存,提高运行效率。

为了形象说明“享元模式”在“对象池”实现和应用,特别准备了模拟了File类,并且模拟了“文件下载”操作。

通过阅读下方代码可以发现:对于**File类,内部状态是pool性和download方法;外部状态是namesrc****(文件名和文件链接)**。借助对象池,实现了File类的复用。

注:为了方便演示,_Javascript实现的是并发操作,Python_实现的是串行操作。输出结果略有不同。

Python3 实现

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
from time import sleep
class ObjectPool: # 通用对象池
def __init__(self):
self.__pool = []
# 创建对象
def create(self, Obj):
# 对象池中没有空闲对象,则创建一个新的对象
# 对象池中有空闲对象,直接取出,无需再次创建
return self.__pool.pop() if len(self.__pool) > 0 else Obj(self)
# 对象回收
def recover(self, obj):
return self.__pool.append(obj)
# 对象池大小
def size(self):
return len(self.__pool)
class File: # 模拟文件对象
def __init__(self, pool):
self.__pool = pool
def download(self): # 模拟下载操作
print('+ 从', self.src, '开始下载', self.name)
sleep(0.1)
print('-', self.name, '下载完成')
# 下载完毕后,将对象重新放入对象池
self.__pool.recover(self)
if __name__ == '__main__':
obj_pool = ObjectPool()
file1 = obj_pool.create(File)
file1.name = '文件1'
file1.src = '<https://download1.com>'
file1.download()
file2 = obj_pool.create(File)
file2.name = '文件2'
file2.src = '<https://download2.com>'
file2.download()
file3 = obj_pool.create(File)
file3.name = '文件3'
file3.src = '<https://download3.com>'
file3.download()
print('*' * 20)
print('下载了3个文件, 但其实只创建了', obj_pool.size(), '个对象')

输出结果(这里为了方便演示直接使用了sleep方法,没有再用多线程模拟):

1
2
3
4
5
6
7
8
+ 从 <https://download1.com> 开始下载 文件1
- 文件1 下载完成
+ 从 <https://download2.com> 开始下载 文件2
- 文件2 下载完成
+ 从 <https://download3.com> 开始下载 文件3
- 文件3 下载完成
********************
下载了3个文件, 但其实只创建了 1 个对象

ES6 实现

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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
// 对象池
class ObjectPool {
constructor() {
this._pool = []; //
}
// 创建对象
create(Obj) {
return this._pool.length === 0
? new Obj(this) // 对象池中没有空闲对象,则创建一个新的对象
: this._pool.shift(); // 对象池中有空闲对象,直接取出,无需再次创建
}
// 对象回收
recover(obj) {
return this._pool.push(obj);
}
// 对象池大小
size() {
return this._pool.length;
}
}
// 模拟文件对象
class File {
constructor(pool) {
this.pool = pool;
}
// 模拟下载操作
download() {
console.log(`+ 从 ${this.src} 开始下载 ${this.name}`);
setTimeout(() => {
console.log(`- ${this.name} 下载完毕`); // 下载完毕后, 将对象重新放入对象池
this.pool.recover(this);
}, 100);
}
}
/****************** 以下是测试函数 **********************/
let objPool = new ObjectPool();
let file1 = objPool.create(File);
file1.name = "文件1";
file1.src = "<https://download1.com>";
file1.download();
let file2 = objPool.create(File);
file2.name = "文件2";
file2.src = "<https://download2.com>";
file2.download();
setTimeout(() => {
let file3 = objPool.create(File);
file3.name = "文件3";
file3.src = "<https://download3.com>";
file3.download();
}, 200);
setTimeout(
() =>
console.log(
`${"*".repeat(
50
)}\\n下载了3个文件,但其实只创建了${objPool.size()}个对象`
),
1000
);

输出结果如下:

1
2
3
4
5
6
7
8
+ 从 <https://download1.com> 开始下载 文件1
+ 从 <https://download2.com> 开始下载 文件2
- 文件1 下载完毕
- 文件2 下载完毕
+ 从 <https://download3.com> 开始下载 文件3
- 文件3 下载完毕
**************************************************
下载了3个文件,但其实只创建了2个对象

代理模式

代理模式的定义:为一个对象提供一种代理以方便对它的访问。

什么是代理模式?

代理模式的定义:为一个对象提供一种代理以方便对它的访问。

代理模式可以解决避免对一些对象的直接访问,以此为基础,常见的有保护代理和虚拟代理。保护代理可以在代理中直接拒绝对对象的访问;虚拟代理可以延迟访问到真正需要的时候,以节省程序开销。

代理模式优缺点

代理模式有高度解耦、对象保护、易修改等优点。

同样地,因为是通过“代理”访问对象,因此开销会更大,时间也会更慢。

代码实现

python3 实现

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
class Image:
def __init__(self, filename):
self.filename = filename
def load_img(self):
print("finish load " + self.filename)
def display(self):
print("display " + self.filename)
# 借助继承来实现代理模式
class ImageProxy(Image):
def __init__(self, filename):
super().__init__(filename)
self.loaded = False
def load_img(self):
if self.loaded == False:
super().load_img()
self.loaded = True
def display(self):
return super().display()
if __name__ == "__main__":
proxyImg = ImageProxy("./js/image.png")
# 只加载一次,其它均被代理拦截
# 达到节省资源的目的
for i in range(0,10):
proxyImg.load_img()
proxyImg.display()

javascript 实现

main.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// main.js
const myImg = {
setSrc(imgNode, src) {
imgNode.src = src;
}
};
// 利用代理模式实现图片懒加载
const proxyImg = {
setSrc(imgNode, src) {
myImg.setSrc(imgNode, "./image.png"); // NO1. 加载占位图片并且将图片放入<img>元素
let img = new Image();
img.onload = () => {
myImg.setSrc(imgNode, src); // NO3. 完成加载后, 更新 <img> 元素中的图片
};
img.src = src; // NO2. 加载真正需要的图片
}
};
let imgNode = document.createElement("img"),
imgSrc =
"<https://upload-images.jianshu.io/upload_images/5486602-5cab95ba00b272bd.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1000/format/webp>";
document.body.appendChild(imgNode);
proxyImg.setSrc(imgNode, imgSrc);

main.html

1
2
3
4
5
6
7
8
9
10
11
12
13
<!-- main.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>每天一个设计模式 · 代理模式</title>
</head>
<body>
<script src="./main.js"></script>
</body>
</html>

桥接模式

桥接模式:将抽象部分和具体实现部分分离,两者可独立变化,也可以一起工作。

什么是桥接模式

桥接模式:将抽象部分和具体实现部分分离,两者可独立变化,也可以一起工作。

在这种模式的实现上,需要一个对象担任“桥”的角色,起到连接的作用。

应用场景

在封装开源库的组件时候,经常会用到这种设计模式。

例如,对外提供暴露一个afterFinish函数,

如果用户有传入此函数, 那么就会在某一段代码逻辑中调用。

这个过程中,组件起到了“桥”的作用,而具体实现是用户自定义。

多语言实现

ES6 实现

JavaScript 中桥接模式的典型应用是:Array对象上的forEach函数。

此函数负责循环遍历数组每个元素,是抽象部分;

而回调函数callback就是具体实现部分。

下方是模拟forEach方法:

1
2
3
4
5
6
7
8
9
10
const forEach = (arr, callback) => {
if (!Array.isArray(arr)) return;
const length = arr.length;
for (let i = 0; i < length; ++i) {
callback(arr[i], i);
}
};
// 以下是测试代码
let arr = ["a", "b"];
forEach(arr, (el, index) => console.log("元素是", el, "位于", index));

python3 实现

和 Js 一样,这里也是模拟一个for_each函数:

它会循环遍历所有的元素,并且对每个元素执行指定的函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from inspect import isfunction
# for_each 起到了“桥”的作用
def for_each(arr, callback):
if isinstance(arr, list) == False or isfunction(callback) == False:
return
for (index, item) in enumerate(arr):
callback(item, index)
# 具体实现部分
def callback(item, index):
print('元素是', item, '; 它的位置是', index)
# 以下是测试代码
if __name__ == '__main__':
arr = ['a', 'b']
for_each(arr, callback)

装饰者模式

装饰者模式:在不改变对象自身的基础上,动态地添加功能代码。

什么是“装饰者模式”?

装饰者模式:在不改变对象自身的基础上,动态地添加功能代码。

根据描述,装饰者显然比继承等方式更灵活,而且不污染原来的代码,代码逻辑松耦合。

应用场景

装饰者模式由于松耦合,多用于一开始不确定对象的功能、或者对象功能经常变动的时候。

尤其是在参数检查参数拦截等场景。

代码实现

ES6 实现

ES6 的装饰器语法规范只是在“提案阶段”,而且不能装饰普通函数或者箭头函数。

下面的代码,addDecorator可以为指定函数增加装饰器。

其中,装饰器的触发可以在函数运行之前,也可以在函数运行之后。

注意:装饰器需要保存函数的运行结果,并且返回。

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
const isFn = fn => typeof fn === "function";
const addDecorator = (fn, before, after) => {
if (!isFn(fn)) {
return () => {};
}
return (...args) => {
let result;
// 按照顺序执行“装饰函数”
isFn(before) && before(...args);
// 保存返回函数结果
isFn(fn) && (result = fn(...args));
isFn(after) && after(...args);
// 最后返回结果
return result;
};
};
/******************以下是测试代码******************/
const beforeHello = (...args) => {
console.log(`Before Hello, args are ${args}`);
};
const hello = (name = "user") => {
console.log(`Hello, ${name}`);
return name;
};
const afterHello = (...args) => {
console.log(`After Hello, args are ${args}`);
};
const wrappedHello = addDecorator(hello, beforeHello, afterHello);
let result = wrappedHello("godbmw.com");
console.log(result);

Python3 实现

python 直接提供装饰器的语法支持。用法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 不带参数
def log_without_args(func):
def inner(*args, **kw):
print("args are %s, %s" % (args, kw))
return func(*args, **kw)
return inner
# 带参数
def log_with_args(text):
def decorator(func):
def wrapper(*args, **kw):
print("decorator's arg is %s" % text)
print("args are %s, %s" % (args, kw))
return func(*args, **kw)
return wrapper
return decorator
@log_without_args
def now1():
print('call function now without args')
@log_with_args('execute')
def now2():
print('call function now2 with args')
if __name__ == '__main__':
now1()
now2()

其实 python 中的装饰器的实现,也是通过“闭包”实现的。

以上述代码中的now1函数为例,装饰器与下列语法等价:

1
2
3
4
5
6
# ....
def now1():
print('call function now without args')
# ...
now_without_args = log_without_args(now1) # 返回被装饰后的 now1 函数
now_without_args() # 输出与前面代码相同

组合模式

组合模式,将对象组合成树形结构以表示“部分-整体”的层次结构。

什么是“组合模式”?

组合模式,将对象组合成树形结构以表示“部分-整体”的层次结构。

  1. 用小的子对象构造更大的父对象,而这些子对象也由更小的子对象构成
  2. 单个对象和组合对象对于用户暴露的接口具有一致性,而同种接口不同表现形式亦体现了多态性

应用场景

组合模式可以在需要针对“树形结构”进行操作的应用中使用,例如扫描文件夹、渲染网站导航结构等等。

代码实现

这里用代码模拟文件扫描功能,封装了FileFolder两个类。在组合模式下,用户可以向Folder类嵌套File或者Folder来模拟真实的“文件目录”的树结构。

同时,两个类都对外提供了scan接口,File下的scan是扫描文件,Folder下的scan是调用子文件夹和子文件的scan方法。整个过程采用的是深度优先

python3 实现

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 File:  # 文件类
def __init__(self, name):
self.name = name
def add(self):
raise NotImplementedError()
def scan(self):
print('扫描文件:' + self.name)
class Folder: # 文件夹类
def __init__(self, name):
self.name = name
self.files = []
def add(self, file):
self.files.append(file)
def scan(self):
print('扫描文件夹: ' + self.name)
for item in self.files:
item.scan()
if __name__ == '__main__':
home = Folder("用户根目录")
folder1 = Folder("第一个文件夹")
folder2 = Folder("第二个文件夹")
file1 = File("1号文件")
file2 = File("2号文件")
file3 = File("3号文件")
# 将文件添加到对应文件夹中
folder1.add(file1)
folder2.add(file2)
folder2.add(file3)
# 将文件夹添加到更高级的目录文件夹中
home.add(folder1)
home.add(folder2)
# 扫描目录文件夹
home.scan()

执行$ python main.py, 最终输出结果是:

1
2
3
4
5
6
扫描文件夹: 用户根目录
扫描文件夹: 第一个文件夹
扫描文件:1号文件
扫描文件夹: 第二个文件夹
扫描文件:2号文件
扫描文件:3号文件

ES6 实现

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
43
44
// 文件类
class File {
constructor(name) {
this.name = name || "File";
}
add() {
throw new Error("文件夹下面不能添加文件");
}
scan() {
console.log("扫描文件: " + this.name);
}
}
// 文件夹类
class Folder {
constructor(name) {
this.name = name || "Folder";
this.files = [];
}
add(file) {
this.files.push(file);
}
scan() {
console.log("扫描文件夹: " + this.name);
for (let file of this.files) {
file.scan();
}
}
}
let home = new Folder("用户根目录");
let folder1 = new Folder("第一个文件夹"),
folder2 = new Folder("第二个文件夹");
let file1 = new File("1号文件"),
file2 = new File("2号文件"),
file3 = new File("3号文件");
// 将文件添加到对应文件夹中
folder1.add(file1);
folder2.add(file2);
folder2.add(file3);
// 将文件夹添加到更高级的目录文件夹中
home.add(folder1);
home.add(folder2);
// 扫描目录文件夹
home.scan();

执行$ node main.js,最终输出结果是:

1
2
3
4
5
6
扫描文件夹: 用户根目录
扫描文件夹: 第一个文件夹
扫描文件: 1号文件
扫描文件夹: 第二个文件夹
扫描文件: 2号文件
扫描文件: 3号文件

适配器模式

适配器模式:为多个不兼容接口之间提供“转化器”。

什么是适配器模式?

适配器模式:为多个不兼容接口之间提供“转化器”。

它的实现非常简单,检查接口的数据,进行过滤、重组等操作,使另一接口可以使用数据即可。

应用场景

当数据不符合使用规则,就可以借助此种模式进行格式转化。

多语言实现

假设编写了不同平台的音乐爬虫,破解音乐数据。而对外向用户暴露的数据应该是具有一致性。

下面,adapter函数的作用就是转化数据格式。

事实上,在我开发的音乐爬虫库music-api-next就采用了下面的处理方法。

因为,网易、QQ、虾米等平台的音乐数据不同,需要处理成一致的数据返回给用户,方便用户调用。

ES6 实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const API = {
qq: () => ({
n: "菊花台",
a: "周杰伦",
f: 1
}),
netease: () => ({
name: "菊花台",
author: "周杰伦",
f: false
})
};
const adapter = (info = {}) => ({
name: info.name || info.n,
author: info.author || info.a,
free: !!info.f
});
/*************测试函数***************/
console.log(adapter(API.qq()));
console.log(adapter(API.netease()));

python 实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
def qq_music_info():
return {
'n': "菊花台",
'a': "周杰伦",
'f': 1
}
def netease_music_info():
return {
'name': "菊花台",
'author': "周杰伦",
'f': False
}
def adapter(info):
result = {}
result['name'] = info["name"] if 'name' in info else info['n']
result['author'] = info['author'] if 'author' in info else info['a']
result['free'] = not not info["f"]
return result
if __name__ == '__main__':
print(adapter(qq_music_info()))
print(adapter(netease_music_info()))

参考