设计模式及js实现(二)结构型模式

23种设计模式根据其目的(模式是用来做什么的)可分为创建型(Creational),结构型(Structural)和行为型(Behavioral)三种:

  • 创建型模式主要用于创建对象。
  • 结构型模式主要用于处理类或对象的组合。
  • 行为型模式主要用于描述对类或对象怎样交互和怎样分配职责。

目录

结构型模式
  - 适配器模式(Adapter)
  - 桥接模式(Bridge)
  - 组合模式(Composite)
  - 装饰模式(Decorator)
  - 外观模式(Facade)
  - 享元模式(Flyweight)
  - 代理模式(Proxy)

结构型模式

适配器模式(Adapter)

adapter

适配器模式(Adapter Pattern)是作为两个不兼容的接口之间的桥梁。这种类型的设计模式属于结构型模式,它结合了两个独立接口的功能。

这种模式涉及到一个单一的类,该类负责加入独立的或不兼容的接口功能。举个真实的例子,读卡器是作为内存卡和笔记本之间的适配器。您将内存卡插入读卡器,再将读卡器插入笔记本,这样就可以通过笔记本来读取内存卡。适配器模式为多个不兼容接口之间提供“转化器”。

在前端开发中,我们可能会遇见这样的场景:当我们试图调用某个模块或者对象的接口时,却发现这个接口的格式不符合我们的需求。这时有两种解决办法:第一种是修改原来的接口实现,但如果原来的代码很复杂,例如是一个库或框架,更改原代码就显得很不现实了。所以这时就需要使用今天所讲的第二种办法:创建一个适配器,将原接口转换为客户希望的另一个接口,客户只需要使用适配器即可。

优缺点

  • 优点: 1、可以让任何两个没有关联的类一起运行。 2、提高了类的复用。 3、增加了类的透明度。 4、灵活性好。
  • 缺点: 1、过多地使用适配器,会让系统非常零乱,不易整体进行把握。比如,明明看到调用的是 A 接口,其实内部被适配成了 B 接口的实现,

在前端项目中,适配器模式的使用场景一般有以下三种情况:库的适配、参数的适配和数据的适配。

js实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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()));

桥接模式(Bridge)

桥接(Bridge)是用于把抽象化与实现化解耦,使得二者可以独立变化。这种类型的设计模式属于结构型模式,它通过提供抽象化和实现化之间的桥接结构,来实现二者的解耦。桥接模式将抽象部分和具体实现部分分离,两者可独立变化,也可以一起工作。

优缺点

  • 优点: 1、抽象和实现的分离。 2、优秀的扩展能力。 3、实现细节对客户透明。
  • 缺点:桥接模式的引入会增加系统的理解与设计难度,由于聚合关联关系建立在抽象层,要求开发者针对抽象进行设计与编程。

在封装开源库的组件时候,经常会用到这种设计模式。例如,对外提供暴露一个afterFinish函数, 如果用户有传入此函数, 那么就会在某一段代码逻辑中调用。这个过程中,组件起到了“桥”的作用,而具体实现是用户自定义。

js实现

桥接模式最常用在事件监控上,如原本这样一个点击事件

1
2
3
4
5
6
7
8
var btn = document.getElementById('btn');

btn.addListener('click', sendReg, false);

function sendReq(e) {
console.log('I clicked ' + e.id);
// ...一串ajax操作...
}

利用桥接模式就会这样写:

1
2
3
4
5
6
7
8
9
10
11
12
13
var btn = document.getElementById('btn');

btn.addListener('click', bridgeHadler, false);

// 桥接函数
function bridgeHadler (e) {
sendReq(e);
}

function sendReq(e) {
console.log('I clicked ' + e.id);
// ...一串ajax操作...
}

或者是Array.prototype.forEach()方法

1
2
3
4
5
6
7
8
9
10
11
12
13
// 模拟Array.prototype.forEach()
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));

特权函数
1
2
3
4
5
6
7
8
9
10
11
12
13
var PublicClass = function() {
//私有变量
var privateMethod = function () {
alert('执行了一个很复杂的操作');
};
// 通过特权函数 去访问这个私用的独立单元
this.bridgeMethod = function () {
return privateMethod();
}
};

var c1 = new PublicClass();
c1.bridgeMethod();
多单元桥接
1
2
3
4
5
6
7
8
9
10
11
12
13
14
var Class1 = function (a,b,c) {
this.a = a;
this.b = b;
this.c = c;
};
var Class2 = function (d,e) {
this.d = d;
this.e = e;
};

var BridgeClass = function (a,b,c,d,e) {
this.Class1 = new Class1(a,b,c);
this.Class2 = new Class1(d,e);
};

组合模式(Composite)

Composite

组合模式(Composite Pattern),又叫部分整体模式,是用于把一组相似的对象当作一个单一的对象。组合模式依据树形结构来组合对象,用来表示部分以及整体层次。这种类型的设计模式属于结构型模式,它创建了对象组的树形结构。这种模式创建了一个包含自己对象组的类。该类提供了修改相同对象组的方式。组合模式,将对象组合成树形结构以表示“部分-整体”的层次结构。

优缺点

  • 优点: 1、高层模块调用简单。 2、节点自由增加。
  • 缺点:在使用组合模式时,其叶子和树枝的声明都是实现类,而不是接口,违反了依赖倒置原则。

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

js实现

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
// 文件类
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();

装饰模式(Decorator)

Decorator

装饰器模式(Decorator Pattern)允许向一个现有的对象添加新的功能,同时又不改变其结构。这种类型的设计模式属于结构型模式,它是作为现有的类的一个包装。装饰者模式:在不改变对象自身的基础上,动态地添加功能代码。

这种模式创建了一个装饰类,用来包装原有的类,并在保持类方法签名完整性的前提下,提供了额外的功能。

优缺点

  • 优点:装饰类和被装饰类可以独立发展,不会相互耦合,装饰模式是继承的一个替代模式,装饰模式可以动态扩展一个实现类的功能。
  • 缺点:多层装饰比较复杂。

装饰者模式由于松耦合,多用于一开始不确定对象的功能、或者对象功能经常变动的时候。 尤其是在参数检查、参数拦截等场景。

js实现

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
function autobind(target, key, descriptor) {
var fn = descriptor.value;
var configurable = descriptor.configurable;
var enumerable = descriptor.enumerable;

// 返回descriptor
return {
configurable: configurable,
enumerable: enumerable,
get: function get() {
// 将该方法绑定this
var boundFn = fn.bind(this);
// 使用Object.defineProperty重新定义该方法
Object.defineProperty(this, key, {
configurable: true,
writable: true,
enumerable: false,
value: boundFn
})

return boundFn;
}
}
}

class Person {
@autobind
getPerson() {
return this;
}
}

let person = new Person();
let { getPerson } = person;

console.log(getPerson() === person); // true

外观模式(Facade)

facade.png
外观模式(Facade Pattern)隐藏系统的复杂性,并向客户端提供了一个客户端可以访问系统的接口。这种类型的设计模式属于结构型模式,它向现有的系统添加一个接口,来隐藏系统的复杂性。

优缺点

  • 优点: 1、减少系统相互依赖。 2、提高灵活性。 3、提高了安全性。
  • 缺点:不符合开闭原则,如果要改东西很麻烦,继承重写都不合适。

外观模式在javascript的应用主要可以分为两类,某块代码反复出现,比如函数a的调用基本都出现在函数b的调用之前,那么可以考虑考虑将这块代码使用外观角色包装一下来优化结构。还有一种就是对于一些浏览器不兼容的API,放置在外观内部进行判断,处理这些问题最好的方式便是将跨浏览器差异全部集中放置到一个外观模式实例中来提供一个对外接口。

js实现

在形式上,外观模式在javascript中就像这样:

1
2
3
4
5
6
7
8
9
10
11

function a(x){
// do something
}
function b(y){
// do something
}
function ab( x, y ){
a(x);
b(y);
}


1
2
3
4
5
6
7
8
9
10
var stopEvent = function(e) {
// 同时阻止事件默认行为和冒泡
e.stopPropagation();
e.preventDefault();
}

// stopEvent 本身就是生产门面
$('#a').click(function(e) {
stopEvent(e);
}

享元模式(Flyweight)

享元模式(Flyweight Pattern)主要用于减少创建对象的数量,以减少内存占用和提高性能。这种类型的设计模式属于结构型模式,它提供了减少对象数量从而改善应用所需的对象结构的方式。

享元模式尝试重用现有的同类对象,如果未找到匹配的对象,则创建新对象。享元模式:运用共享技术来减少创建对象的数量,从而减少内存占用、提高性能。

享元模式提醒我们将一个对象的属性划分为内部和外部状态。

  • 内部状态:可以被对象集合共享,通常不会改变
  • 外部状态:根据应用场景经常改变
    享元模式是利用时间换取空间的优化模式。

优缺点

  • 优点:大大减少对象的创建,降低系统的内存,使效率提高。
  • 缺点:提高了系统的复杂度,需要分离出外部状态和内部状态,而且外部状态具有固有化的性质,不应该随着内部状态的变化而变化,否则会造成系统的混乱。

只要是需要大量创建重复的类的代码块,均可以使用享元模式抽离内部/外部状态,减少重复类的创建。

js实现

某商家有 50 种男款内衣和 50 种款女款内衣,要展示它们

方案一:造 50 个塑料男模和 50 个塑料女模,让他们穿上展示,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const Model = function(gender, underwear) {
this.gender = gender
this.underwear = underwear
}

Model.prototype.takephoto = function() {
console.log(`${this.gender}穿着${this.underwear}`)
}

for (let i = 1; i < 51; i++) {
const maleModel = new Model('male', `第${i}款衣服`)
maleModel.takephoto()
}

for (let i = 1; i < 51; i++) {
const female = new Model('female', `第${i}款衣服`)
female.takephoto()
}

方案二:造 1 个塑料男模特 1 个塑料女模特,分别试穿 50 款内衣

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const Model = function(gender) {
this.gender = gender
}

Model.prototype.takephoto = function() {
console.log(`${this.gender}穿着${this.underwear}`)
}

const maleModel = new Model('male')
const femaleModel = new Model('female')

for (let i = 1; i < 51; i++) {
maleModel.underwear = `第${i}款衣服`
maleModel.takephoto()
}

for (let i = 1; i < 51; i++) {
femaleModel.underwear = `第${i}款衣服`
femaleModel.takephoto()
}

进一步改善

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
const Model = function(gender) {
this.gender = gender
}

Model.prototype.takephoto = function() {
console.log(`${this.gender}穿着${this.underwear}`)
}

const modelFactory = (function() { // 优化第一点
const modelGender = {}
return {
createModel: function(gender) {
if (modelGender[gender]) {
return modelGender[gender]
}
return modelGender[gender] = new Model(gender)
}
}
}())

const modelManager = (function() {
const modelObj = {}
return {
add: function(gender, i) {
modelObj[i] = {
underwear: `第${i}款衣服`
}
return modelFactory.createModel(gender)
},
copy: function(model, i) { // 优化第二点
model.underwear = modelObj[i].underwear
}
}
}())

for (let i = 1; i < 51; i++) {
const maleModel = modelManager.add('male', i)
modelManager.copy(maleModel, i)
maleModel.takephoto()
}

for (let i = 1; i < 51; i++) {
const femaleModel = modelManager.add('female', i)
modelManager.copy(femaleModel, i)
femaleModel.takephoto()
}

ES6 demo:

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
60
61
62
63
64
65
66
67
68
// 对象池
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
);

代理模式(Proxy)

proxy.png

在代理模式(Proxy Pattern)中,一个类代表另一个类的功能。这种类型的设计模式属于结构型模式。

在代理模式中,我们创建具有现有对象的对象,以便向外界提供功能接口。代理模式的定义:为一个对象提供一种代理以方便对它的访问。

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

优缺点

  • 优点: 1、职责清晰,高度解耦。 2、高扩展性。 3、智能化。
  • 缺点: 1、由于在客户端和真实主题之间增加了代理对象,因此有些类型的代理模式可能会造成请求的处理速度变慢。 2、实现代理模式需要额外的工作,有些代理模式的实现非常复杂。

js实现

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
// 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);

ES6 demo:

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
class getDelivery {
constructor() {

}

gets(a) {
console.log(`获取快递有:${a}`)
}
}


class proxy extends getDelivery {
constructor() {
super();
}

proxyGets(name) {
let fn1 = () => {
setTimeout(() => {
//some fns of fn1
super.gets('中通快递')
}, 1000)
}
let fn2 = () => {
setTimeout(() => {
//some fns of fn2
super.gets('EMS')
}, 2000)
}
let fn3 = () => {
setTimeout(() => {
//some fns of fn3
super.gets('顺丰')
}, 3000)
}
let deliver = {'中通': fn1, 'EMS': fn2, '顺丰': fn3}[name];
return deliver();
}
}

proxy.prototype.proxyGets('中通')