javaScript设计模式-FlyWeight(享元)模式

FlyWeight(享元)模式

Flyweight 模式是一种经典的结构型解决方案,用于优化重复、缓存及数据共享效率低的代码。它旨在通过与相关的对象共享尽可能多的数据来减少应用程序中内存的使用。

Flyweight 模式的应用方式有两种。

  1. 第一种是用于数据层,处理内存中保存的大量相似对象的共享数据。
  2. 第二种是用于 DOM 层,Flyweight 可以用作中央事件处理程序附加到这个父容器上。

Flyweight 和共享数据

在 Flyweight 模式中,有个有关两个状态的概念–内部和外部。对象中的内部方法可能需要内部信息,没有内部信息,它们就绝对无法正常运行。但外部信息是可以被删除的或是可以存储在外部的。

具有相同内部数据的对象可以被替换为一个由 factory 方法创建的单一共享对象。这使我们可以极大减少存储隐式数据的总数量。

实现经典 Flyweight(享元)

在这个实现中我们将利用三种类型的 Flyweight 组件,它们是:

  1. Flyweight(享元)
    描述一个接口,通过这个接口 flyweight 可以接受并作用于外部状态。
  2. Concrete flyweight(具体享元)
    实现 Flyweight 接口,并存储内部状态。Concrete Flyweight 对象必须是可共享的,并能够控制外部状态。
  3. Flyweight factory(享元工厂)
    创建并管理 flyweight 对象。确保合理共享 flyweight,并将它们当作一组对象进行管理,并且如果我们需要单个实例时,可以查询这些对象。如果该对象已经存在则直接返回,否则,创建新对象并返回。

它们与实现中下列定义相对应:

  • CoffeeOrder: 享元
  • CoffeeFlavor: 具体享元
  • CoffeeOrderContext: 辅助器
  • CoffeeFlavorFactory: 享元工厂
  • testFlyweight:享元的应用
鸭子补丁“实现”

鸭子补丁(Duck punching)使我们无需修改运行时源,就可以扩展一种语言或解决方案的功能。

Function.prototype.implementsFor 作用于一个对象构造函数,并将接受一个父类(函数)或对象,或者使用普通继承(函数)或虚拟继承(对象)来继承它。

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
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
// 在jS中模拟纯虚拟继承 implement
Function.prototype.implementsFor = function(parentClassOrObject) {
if (parentClassOrObject.constructor === Function) {
// 正常继承
this.prototype = new parentClassOrObject();
this.prototype.constructor = this;
this.prototype.parent = parentClassOrObject.prototype;
console.log("正常继承");
} else {
// 纯虚拟继承
this.prototype = parentClassOrObject;
this.prototype.constructor = this;
this.prototype.parent = parentClassOrObject;
console.log("纯虚拟继承");
}

return this;
};

/**
* 通过使一个函数显示地继承一个接口,可以用它来为缺少的implements关键字打上补丁。在下面的代码力,CoffeeFlavor实现了CoffeeOrder接口,且必须包含它的接口方法,以便将功能的实现赋值给对象。
*/

// 享元对象
var CoffeeOrder = {
// 接口
serveCoffee: function(context) {},
getFlavor: function() {}
};

// 实现CoffeeOrder的具体享元对象
function CoffeeFlavor(newFlavor) {
var flavor = newFlavor;

// 如果已经为某一功能定义了接口,则实现该功能
if (typeof this.getFlavor === "function") {
// 重写CoffeeOrder的方法
this.getFlavor = function() {
return flavor;
};
}

if (typeof this.serveCoffee === "function") {
// 重写serveCoffee的方法
this.serveCoffee = function(context) {
console.log(
"Serving Coffee flavor " +
flavor +
" to table number " +
context.getTable()
);
};
}
}

// 为CoffeeOrder实现接口
CoffeeFlavor.implementsFor(CoffeeOrder);

// 处理coffee订单的table数
function CoffeeOrderContext(tableNumber) {
return {
getTable: function() {
return tableNumber;
}
};
}

// 享元工厂对象
function CoffeeFlavorFactory() {
var flavors = [];

return {
getCoffeeFlavor: function(flavorName) {
var flavor = flavors[flavorName];
if (flavor === undefined) {
flavor = new CoffeeFlavor(flavorName);
flavors.push([[flavorName], flavor]);
}

return flavor;
},

getTotalCoffeeFlavorsMade: function() {
return flavors.length + 1;
}
};
}

// 样例
function testFlyweight() {
// 已订购的flavor
var flavors = new CoffeeFlavor(),
// 订单table
tables = new CoffeeOrderContext(),
// 订单数量
ordersMade = 0,
// TheCoffeeFlavorFactory 实例
flavorFactory;
console.log(flavors);
function takeOrders(flavorIn, table) {
console.log("ordersMade ", ordersMade);
flavors[ordersMade] = flavorFactory.getCoffeeFlavor(flavorIn);
tables[ordersMade++] = new CoffeeOrderContext(table);
}

flavorFactory = new CoffeeFlavorFactory();
takeOrders("Cappuccino", 2);
takeOrders("Cappuccino", 2);
takeOrders("Frappe", 1);
takeOrders("Frappe", 1);
takeOrders("Xpresso", 1);
takeOrders("Frappe", 897);
takeOrders("Cappuccino", 97);
takeOrders("Cappuccino", 97);
takeOrders("Frappe", 3);
takeOrders("Xpresso", 3);
takeOrders("Cappuccino", 3);
takeOrders("Xpresso", 552);
takeOrders("Cappuccino", 121);
takeOrders("Xpresso", 121);

for (var i = 0; i < ordersMade; ++i) {
flavors[i].serveCoffee(tables[i]);
}

console.log(" ");
console.log(
"total CoffeeFlavor objects made: " +
flavorFactory.getTotalCoffeeFlavorsMade()
);
}

转换代码以使用 Flyweight(享元)模式

接下来,通过实现一个系统来管理图书馆中的所有书籍,让我们来继续了解一下享元。每本书的重要元数据可以被分解成如下形式:

  • ID
  • Title
  • Author
  • Genre
  • Page count
  • Publisher ID
  • Publisher ID
  • ISBN
    我们还将需要使用以下属性来跟踪哪些成员已借出了哪些书籍,借书日期以及预计返还的日期。
  • checkoutDate
  • checkoutMember
  • dueReturnDate
  • availablity
    因此每本书在使用享元模式进行优化之前,都会按如下方式表示:
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
var Book = function(
id,
title,
author,
genre,
pageCount,
publisherID,
ISBN,
checkoutDate,
checkoutMember,
dueReturnDate,
availability
) {
this.id = id;
this.title = title;
this.author = author;
this.genre = genre;
this.pageCount = pageCount;
this.publisherID = publisherID;
this.ISBN = ISBN;
this.checkoutDate = checkoutDate;
this.checkoutMember = checkoutMember;
this.dueReturnDate = dueReturnDate;
this.availability = availability;
};

Book.prototype = {
getTitle: function() {
return this.title;
},

getAuthor: function() {
return this.author;
},

getISBN: function() {
return this.ISBN;
},

// ...

updateCheckoutStatus: function(
bookID,
newStatus,
checkoutDate,
checkoutMember,
newReturnDate
) {
this.id = bookID;
this.availability = newStatus;
this.checkoutDate = checkoutDate;
this.checkoutMember = checkoutMember;
this.dueReturnDate = newReturnDate;
},

extendCheckoutPeriod: function(bookID, newReturnDate) {
this.id = bookID;
this.dueReturnDate = newReturnDate;
},

isPastDue: function(bookID) {
var currentDate = new Date();
return currentDate.getTime() > Date.parse(this.dueReturnDate);
}
};

刚开始对于少量书籍可能是行得通的,但是,当图书馆扩大到拥有一个更大的库存,并且每本书都有多个版本和副本时,就会发现随着时间的推移,管理系统运行得越来越慢。使用数以千计的书籍对象可能会淹没可用的内存,但可以使用享元模式优化系统来改善这个问题。

下面书籍元数据组合的单个实例将在指定书名的书籍副本之间共享。

1
2
3
4
5
6
7
8
var Book = function(title, author, genre, pageCount, publisherID, ISBN) {
this.title = title;
this.author = author;
this.genre = genre;
this.pageCount = pageCount;
this.publisherID = publisherID;
this.ISBN = ISBN;
};

正如我们可以看到的,外部状态已被删除。图书馆借出有关的所有事情都将转移给管理器,由于对象数据现在已被分割,可以使用工厂进行实例化。

基本工厂

首先,必须检查一下指定书名的书是否已在系统内部创建。如果已经创建,则返回它;如果没有,就会创建并存储这本新书,以便以后可以访问它。这确保我们仅为每一个特定的内部数据块创建一个拷贝。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 书籍工厂单例
var BookFactory = function() {
var existingBooks = {},
existingBook;

return {
createBook: function(title, author, genre, pageCount, publisherID, ISBN) {
// 如果书籍之前已经创建,则找出并返回它
existingBook = existingBooks[ISBN];
if (!!existingBook) {
return existingBook;
} else {
var book = new Book(title, author, genre, pageCount, publisherID, ISBN);
existingBooks[ISBN] = book;
return book;
}
}
};
};

管理外部状态

接下来,我们需要存储从 Book 对象中删除的状态。幸运的是,可以使用管理器(我们会将它定义为一个单例)来封装它们,一个 Book 对象和借书成员的组合将被称为书籍记录。管理器会将它们存储起来,它还包括在 Book 类享元优化期间我们排除的与借出有关的逻辑。

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
// 书籍记录管理器单例
var BookRecordManager = function() {
var bookRecordDatebase = {};
return {
// 添加新书到图书馆系统
addBookRecord: function(
id,
title,
author,
genre,
pageCount,
publisherID,
ISBN,
checkoutDate,
checkoutMember,
dueReturnDate,
availability
) {
var book = BookFactory.createBook(
title,
author,
genre,
pageCount,
publisherID,
ISBN
);

bookRecordDatebase[id] = {
checkoutMember,
checkoutDate,
dueReturnDate,
availability,
book
};
},

updateCheckoutStatus: function(
bookID,
newStatus,
checkoutDate,
checkoutMember,
newReturnDate
) {
var record = bookRecordDatebase[bookID];
record.availability = newStatus;
record.checkoutDate = checkoutDate;
record.checkoutMember = checkoutMember;
record.dueReturnDate = newReturnDate;
},

extendCheckoutPeriod: function(bookID, newReturnDate) {
bookRecordDatebase[bookID].dueReturnDate = newReturnDate;
},

isPastDue: function(bookID) {
var currentDate = new Date();
return (
currentDate.getTime() >
Date.parse(bookRecordDatebase[bookID].dueReturnDate)
);
}
};
};

这些代码修改的结果是,从 Book 类中提取的所有数据,现在被存储在 BookManager 单例的属性中,这比我们以前使用大量对象时的效率要高很多。现在与书籍出借相关的方法在这里成为了基础,因为它们处理的是外部数据,而不是内部数据。

下面是个更简单的例子

这是未使用享元模式前的结构,模拟上传的功能。

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
69
70
71
72
73
74
75
var id = 0;
window.startUpload = function(uploadType, files) {
console.log(files);
for (var i = 0, file; (file = files[i++]); ) {
// 有多少个文件就有多少个对象
var uploadObj = new Upload(uploadType, file.fileName, file.fileSize);
uploadObj.init(id++);
}
};

var Upload = function(uploadType, fileName, fileSize) {
this.fileName = fileName;
this.fileSize = fileSize;
this.dom = null;
};

Upload.prototype.init = function(id) {
var that = this;
this.id = id;
this.dom = document.createElement("div");
this.dom.innerHTML =
this.id +
" <span>文件名称:" +
this.fileName +
", 文件大小:" +
this.fileSize +
"</span>" +
'<button class="delFile">删除</button>';
this.dom.querySelector(".delFile").onclick = function() {
that.delFile();
};
document.body.appendChild(this.dom);
};

Upload.prototype.delFile = function() {
if (this.fileSize < 3000) {
return this.dom.parentNode.removeChild(this.dom);
}

if (window.confirm("确定要删除该文件吗?" + this.fileName)) {
return this.dom.parentNode.removeChild(this.dom);
}
};
startUpload("plugin", [
{
fileName: "1.txt",
fileSize: 1000
},
{
fileName: "2.html",
fileSize: 3000
},
{
fileName: "3.txt",
fileSize: 5000
},
{
fileName: "66.txt",
fileSize: 4000
}
]);
startUpload("flash", [
{
fileName: "4.txt",
fileSize: 1000
},
{
fileName: "5.html",
fileSize: 3000
},
{
fileName: "6.txt",
fileSize: 5000
}
]);

使用享元后

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
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
var Upload = function(uploadType) {
this.uploadType = uploadType;
};

Upload.prototype.delFile = function(id) {
uploadManager.setExternalState(id, this);
if (this.fileSize < 3000) {
return this.dom.parentNode.removeChild(this.dom);
}

if (window.confirm("确定要删除文件吗? " + this.fileName)) {
return this.dom.parentNode.removeChild(this.dom);
}
};
// 通过工厂和单例的模式将对象个数变成了两个。
var UploadFactory = (function() {
var createdFlyWeightObjs = {};

return {
create: function(uploadType) {
if (createdFlyWeightObjs[uploadType]) {
return createdFlyWeightObjs[uploadType];
}
return (createdFlyWeightObjs[uploadType] = new Upload(uploadType));
}
};
})();

var uploadManager = (function() {
var uploadDatabase = {};
return {
add: function(id, uploadType, fileName, fileSize) {
var flyWeightObj = UploadFactory.create(uploadType);
var dom = document.createElement("div");
dom.innerHTML =
"<span>文件名称:" +
fileName +
", 文件大小:" +
fileSize +
"</span>" +
' <button class="delFile">删除</button>';
dom.querySelector(".delFile").onclick = function() {
flyWeightObj.delFile(id);
};

document.body.appendChild(dom);
uploadDatabase[id] = {
fileName: fileName,
fileSize: fileSize,
dom
};
return flyWeightObj;
},

setExternalState: function(id, flyWeightObj) {
var uploadData = uploadDatabase[id];
for (var i in uploadData) {
flyWeightObj[i] = uploadData[i];
}
}
};
})();

var id = 0;
window.startUpload = function(uploadType, files) {
for (var i = 0, file; (file = files[i++]); ) {
var uploadObj = uploadManager.add(
++id,
uploadType,
file.fileName,
file.fileSize
);
}
};

startUpload("plugin", [
{
fileName: "1.txt",
fileSize: 1000
},
{
fileName: "2.html",
fileSize: 3000
},
{
fileName: "3.txt",
fileSize: 5000
}
]);
startUpload("flash", [
{
fileName: "4.txt",
fileSize: 1000
},
{
fileName: "5.html",
fileSize: 3000
},
{
fileName: "6.txt",

fileSize: 5000
}
]);

Flyweight(享元)模式和 DOM

文档对象模型(DOM)支持两种方式让对象检测事件:自上而下(事件捕捉)和自下而上(事件冒泡)。

在事件捕捉中,事件首先被最外层的元素捕捉,然后传播到最里面的元素。在事件冒泡中,事件被捕捉并传递给里面的元素,然后传播到外部元素。

试着用池塘的方式思考一下享元。一条鱼张开它的嘴(事件),气泡升到表面(冒泡),当气泡到达表面(动作)时,一只坐在顶部的苍蝇飞走了。在本例中,我们可以很容易地把鱼张开嘴转换成点一个按钮,气泡转换成冒泡效应,苍蝇飞走可以转换成运行一些功能。