# 亨元模式
享元模式是一個優化重復、緩慢和低效數據共享代碼的經典結構化解決方案。它的目標是以相關對象盡可能多的共享數據,來減少應用程序中內存的使用(例如:應用程序的配置、狀態等)。
此模式最先由Paul Calder 和 Mark Linton在1990提出,并用拳擊等級中少于112磅體重的等級名稱來命名。享元(“Flyweight”英語中的輕量級)的名稱本身是從以幫以助我們完成減少重量(內存標記)為目標的重量等級推導出的。
實際應用中,輕量級的數據共享采集被多個對象使用的相似對象或數據結構,并將這些數據放置于單個的擴展對象中。我們可以把它傳遞給依靠這些數據的對象,而不是在他們每個上面都存儲一次。
## 使用享元
有兩種方法來使用享元。第一種是數據層,基于存儲在內存中的大量相同對象的數據共享的概念。第二種是DOM層,享元模式被作為事件管理中心,以避免將事件處理程序關聯到我們需要相同行為父容器的所有子節點上。 享元模式通常被更多的用于數據層,我們先來看看它。
## 享元和數據共享
對于這個應用程序而言,圍繞經典的享元模式有更多需要我們意識到的概念。享元模式中有一個兩種狀態的概念——內在和外在。內在信息可能會被我們的對象中的內部方法所需要,它們絕對不可以作為功能被帶出。外在信息則可以被移除或者放在外部存儲。
帶有相同內在數據的對象可以被一個單獨的共享對象所代替,它通過一個工廠方法被創建出來。這允許我們去顯著降低隱式數據的存儲數量。
個中的好處是我們能夠留心于已經被初始化的對象,讓只有不同于我們已經擁有的對象的內在狀態時,新的拷貝才會被創建。
我們使用一個管理器來處理外在狀態。如何實現可以有所不同,但針對此的一種方法就是讓管理器對象包含一個存儲外在狀態以及它們所屬的享元對象的中心數據庫。
## 經典的享元實現
近幾年享元模式已經在Javascript中得到了深入的應用,我們會用到的許多實現方式其靈感來自于Java和C++的世界。
我們第一個要來看的關于享元模式的代碼是我的對來自[維基百科](http://en.wikipedia.org/wiki/Flyweight_pattern)的針對享元模式的 Java 示例的 Javascript 實現。
在這個實現中我們將要使用如下所列的三種類型的享元組件:
* 享元對應的是一個接口,通過此接口能夠接受和控制外在狀態。
* 構造享元來實際的實際的實現接口,并存儲內在狀態。構造享元須是能夠被共享的,并且具有操作外在狀態的能力。
* 享元工廠負責管理享元對象,并且也創建它們。它確保了我們的享元對象是共享的,并且可以對其作為一組對象進行管理,這一組對象可以在我們需要的時候查詢其中的單個實體。如果一個對象已經在一個組里面創建好了,那它就會返回該對象,否則它會在對象池中新創建一個,并且返回之。
這些對應于我們實現中的如下定義:
* CoffeeOrder:享元
* CoffeeFlavor:構造享元
* CoffeeOrderContext:輔助器
* CoffeeFlavorFactory:享元工廠
* testFlyweight:對我們享元的使用
## 鴨式沖減的 “implements”
鴨式沖減允許我們擴展一種語言或者解決方法的能力,而不需要變更運行時的源。由于接下的方案需要使用一個Java關鍵字“implements”來實現接口,而在Javascript本地看不到這種方案,那就讓我們首先來對它進行鴨式沖減。
Function.prototype.implementsFor 在一個對象構造器上面起作用,并且將接受一個父類(函數—)或者對象,而從繼承于普通的繼承(對于函數而言)或者虛擬繼承(對于對象而言)都可以。
// Simulate pure virtual inheritance/"implement" keyword for JS Function.prototype.implementsFor = function( parentClassOrObject ){ if ( parentClassOrObject.constructor === Function ) { // Normal Inheritance this.prototype = new parentClassOrObject(); this.prototype.constructor = this; this.prototype.parent = parentClassOrObject.prototype; } else { // Pure Virtual Inheritance this.prototype = parentClassOrObject; this.prototype.constructor = this; this.prototype.parent = parentClassOrObject; } return this; };
我們可以通過讓一個函數明確的繼承自一個接口來彌補implements關鍵字的缺失。下面,為了使我們得以去分配支持一個對象的這些實現的功能,CoffeeFlavor實現了CoffeeOrder接口,并且必須包含其接口的方法。
~~~
// Flyweight object
var CoffeeOrder = {
// Interfaces
serveCoffee:function(context){},
getFlavor:function(){}
};
// ConcreteFlyweight object that creates ConcreteFlyweight
// Implements CoffeeOrder
function CoffeeFlavor( newFlavor ){
var flavor = newFlavor;
// If an interface has been defined for a feature
// implement the feature
if( typeof this.getFlavor === "function" ){
this.getFlavor = function() {
return flavor;
};
}
if( typeof this.serveCoffee === "function" ){
this.serveCoffee = function( context ) {
console.log("Serving Coffee flavor "
+ flavor
+ " to table number "
+ context.getTable());
};
}
}
// Implement interface for CoffeeOrder
CoffeeFlavor.implementsFor( CoffeeOrder );
// Handle table numbers for a coffee order
function CoffeeOrderContext( tableNumber ) {
return{
getTable: function() {
return tableNumber;
}
};
}
function CoffeeFlavorFactory() {
var flavors = {},
length = 0;
return {
getCoffeeFlavor: function (flavorName) {
var flavor = flavors[flavorName];
if (flavor === undefined) {
flavor = new CoffeeFlavor(flavorName);
flavors[flavorName] = flavor;
length++;
}
return flavor;
},
getTotalCoffeeFlavorsMade: function () {
return length;
}
};
}
// Sample usage:
// testFlyweight()
function testFlyweight(){
// The flavors ordered.
var flavors = new CoffeeFlavor(),
// The tables for the orders.
tables = new CoffeeOrderContext(),
// Number of orders made
ordersMade = 0,
// The CoffeeFlavorFactory instance
flavorFactory;
function takeOrders( flavorIn, table) {
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", 96);
takeOrders("Frappe", 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());
} <span style="line-height:1.5;font-family:'sans serif', tahoma, verdana, helvetica;font-size:10pt;"></span>
~~~
## 轉換代碼為使用享元模式
接下來,讓我們通過實現一個管理一個圖書館中所有書籍的系統來繼續觀察享元。分析得知每一本書的重要元數據如下:
* ID
* 標題
* 作者
* 類型
* 總頁數
* 出版商ID
* ISBN
我們也將需要下面一些屬性,來跟蹤哪一個成員是被借出的一本特定的書,借出它們的日期,還有預計的歸還日期。
* 借出日期
* 借出的成員
* 規定歸還時間
* 可用性
~~~
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;
},
// For brevity, other getters are not shown
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 );
}
};
~~~
這對于最初小規模的藏書可能工作得還好,然而當圖書館擴充至每一本書的多個版本和可用的備份,這樣一個大型的庫存,我們會發現管理系統的運行隨著時間的推移會越來越慢。使用成千上萬的書籍對象可能會壓倒內存,而我們可以通過享元模式的提升來優化我們的系統。
現在我們可以像下面這樣將我們的數據分離成為內在和外在的狀態:同書籍對象(標題,版權歸屬)相關的數據是內在的,而借出數據(借出成員,規定歸還日期)則被看做是外在的。這實際上意味著對于每一種書籍屬性的組合僅需要一個書籍對象。這仍然具有相當大的數量,但相比之前已經得到大大的縮減了。
下面的書籍元數據組合的單一實體將在所有帶有一個特定標題的書籍拷貝中共享。
~~~
// Flyweight optimized version
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;
};
~~~
如我們所見,外在狀態已經被移除了。從圖書館借出所要做的一切都被轉移到一個管理器中,由于對象數據現在是分段的,工廠可以被用來做實例化。
## 一個基本工廠
現在讓我們定義一個非常基本的工廠。我們用它做的工作是,執行一個檢查來看看一本給定標題的書是不是之前已經在系統內創建過了;如果創建過了,我們就返回它 - 如果沒有,一本新書就會被創建并保存,使得以后可以訪問它。這確保了為每一條本質上唯一的數據,我們只創建了一份單一的拷貝:
~~~
// Book Factory singleton
var BookFactory = (function () {
var existingBooks = {}, existingBook;
return {
createBook: function ( title, author, genre, pageCount, publisherID, ISBN ) {
// Find out if a particular book meta-data combination has been created before
// !! or (bang bang) forces a boolean to be returned
existingBook = existingBooks[ISBN];
if ( !!existingBook ) {
return existingBook;
} else {
// if not, let's create a new instance of the book and store it
var book = new Book( title, author, genre, pageCount, publisherID, ISBN );
existingBooks[ISBN] = book;
return book;
}
}
};
});
~~~
## 管理外在狀態
下一步,我們需要將那些從Book對象中移除的狀態存儲到某一個地方——幸運的是一個管理器(我們會將其定義成一個單例)可以被用來封裝它們。書籍對象和借出這些書籍的圖書館成員的組合將被稱作書籍借出記錄。這些我們的管理器都將會存儲,并且也包含我們在對Book類進行享元優化期間剝離的同借出相關的邏輯。
~~~
// BookRecordManager singleton
var BookRecordManager = (function () {
var bookRecordDatabase = {};
return {
// add a new book into the library system
addBookRecord: function ( id, title, author, genre, pageCount, publisherID, ISBN, checkoutDate, checkoutMember, dueReturnDate, availability ) {
var book = bookFactory.createBook( title, author, genre, pageCount, publisherID, ISBN );
bookRecordDatabase[id] = {
checkoutMember: checkoutMember,
checkoutDate: checkoutDate,
dueReturnDate: dueReturnDate,
availability: availability,
book: book
};
},
updateCheckoutStatus: function ( bookID, newStatus, checkoutDate, checkoutMember, newReturnDate ) {
var record = bookRecordDatabase[bookID];
record.availability = newStatus;
record.checkoutDate = checkoutDate;
record.checkoutMember = checkoutMember;
record.dueReturnDate = newReturnDate;
},
extendCheckoutPeriod: function ( bookID, newReturnDate ) {
bookRecordDatabase[bookID].dueReturnDate = newReturnDate;
},
isPastDue: function ( bookID ) {
var currentDate = new Date();
return currentDate.getTime() > Date.parse( bookRecordDatabase[bookID].dueReturnDate );
}
};
});
~~~
這些改變的結果是所有從Book類中擷取的數據現在被存儲到了BookManager單例(BookDatabase)的一個屬性之中——與我們以前使用大量對象相比可以被認為是更加高效的東西。同書籍借出相關的方法也被設置在這里,因為它們處理的數據是外在的而不內在的。
這個過程確實給我們最終的解決方法增加了一點點復雜性,然而同已經明智解決的數據性能問題相比,這只是一個小擔憂,如果我們有同一本書的30份拷貝,現在我們只需要存儲它一次就夠了。每一個函數也會占用內存。使用享元模式這些函數只在一個地方存在(就是在管理器上),并且不是在每一個對象上面,這節約了內存上的使用。
## 享元模式和DOM
DOM(文檔對象模型)支持兩種允許對象偵聽事件的方法——自頂向下(事件捕獲)或者自底向下(時間冒泡)。 在事件捕獲中,事件一開始會被最外面的元素捕獲,并且傳播到最里面的元素。在事件冒泡中,事件被捕獲并且被賦給了最里面的元素,然后傳播到最外面的元素。
在此背景下描述享元模式的最好隱喻來自Gary Chisholm寫的文章,這里摘錄了一點點:
> 嘗試用一種池塘的方式思考享元模式。一只魚張開了它的嘴巴(事件發生了),泡泡一直要上升到表面(冒泡),當泡泡到達表面時,停泊在頂部的一直蒼蠅飛走了(動作執行)。在這個示例中我們能夠很容易的將魚張開嘴巴轉換為按鈕被點擊了一下,將泡泡轉換為冒泡效果,而蒼蠅飛走了表示一些需要運行的函數。
冒泡被引入用來處理單個事件(比如:一次點擊)可能會由在DOM層級中的不同級別的多個事件處理器處理,這樣的場景。這在哪里發生了,事件冒泡就會為在盡可能最低的級別定義的事件處理器執行。從那里開始,事件向上冒泡,一直到包含比應該包含的更高層級的元素。
享元模式可用來進一步調整事件冒泡過程,這我們很快就將會看到。
### 例子1:集中式事件處理
一起來看看我們第一例子,當用戶有個動作(如點擊或是鼠標移動)時我們將有很多相似的文檔對象以及相似的行為要處理。一般情況下,當我們構建手風琴式控件,菜單以及其它列表控件時,就會在每一個超鏈接元素父容器里綁定點擊事件(如,$('ul li a').on(..)(jQuery代碼,譯者注))。我們可以方便的在可以監聽事件容器里添加Flyweight,而不是在很多元素里綁定點擊事件。這樣就可處理或是簡單或是復雜的需求。
提到組件的類型,經常會涉及到很多部分都有同樣重復的標簽(如,手風琴式控件),這是個好機會,每個元素都有可能被點擊的行為,而且基本上用相同的類。我們可以用Flyweight來構建一個基本的手風琴控件。
這里我們使用一個stateManager命名空間來封裝我們的享元邏輯,同時使用jQuery來把初始點擊事件綁定到一個div容器上。為了確保頁面上沒有其他程序邏輯把類似的處理器綁定到該容器上,首先使用了一個unbind事件。
現在明確的確立一下容器中的那個子元素會被點擊,我們使用一次對target的檢查來提供對被點擊元素的引用,而不管它的父元素是誰。然后我們利用該信息來處理點擊事件,而實際上不需要在頁面裝載時把該事件綁定到具體的子元素上。
#### HTML
~~~
<div id="container">
<div class="toggle" href="#">More Info (Address)
<span class="info">
This is more information
</span></div>
<div class="toggle" href="#">Even More Info (Map)
<span class="info">
<iframe src="http://www.map-generator.net/extmap.php?name=London&address=london%2C%20england&width=500...gt;"</iframe>
</span>
</div>
</div>
~~~
#### JavaScript
~~~
var stateManager = {
fly: function () {
var self = this;
$( "#container" ).unbind().on( "click" , function ( e ) {
var target = $( e.originalTarget || e.srcElement );
if ( target.is( "div.toggle") ) {
self.handleClick( target );
}
});
},
handleClick: function ( elem ) {
elem.find( "span" ).toggle( "slow" );
}
};
~~~
這樣做的好處是,我們把許多不相關的動作轉換為一個可以共享的動作(也許會保存在內存中)。
### 示例2:使用享元進行性能優化
在我們的第二個示例中,我們將會引述通過使用jQuery的享元可以獲得的一些更多的性能上的收獲。
Jame Padolsey 以前寫過一篇叫做76比特的文章,講述更快的jQuery,在其中他提醒我們每一次jQuery觸發了一個回調,不管是什么類型(過濾器,每一個,事件處理器),我們都能夠通過this關鍵字訪問函數的上下文(與它相關的DOM元素)。
不幸的是,我們中的許多人已經習慣將this封裝到$()或者jQuery()中的想法,這意味著新的jQuery實體沒必要每次都被構造出來,而是簡單的這樣做:
~~~
$("div").on( "click", function () {
console.log( "You clicked: " + $( this ).attr( "id" ));
});
// we should avoid using the DOM element to create a
// jQuery object (with the overhead that comes with it)
// and just use the DOM element itself like this:
$( "div" ).on( "click", function () {
console.log( "You clicked:" + this.id );
});
~~~
James想要下面的場景中使用jQuery的jQuery.text,然而他不能茍同一個新的jQuery對象必須在每次迭代中創建的概念。
~~~
$( "a" ).map( function () {
return $( this ).text();
});
~~~
現在就使用jQuery的工具方法進行多余的包裝而言,使用jQuery.methodName(如,jQuery.text)比jQuery.fn.methodName(如,jQuery.fn.text)更好,這里methodName代表了一種使用的工具,如each()或者text。這避免了調用更深遠級別的抽象,或者每一次當我們的函數被調用時就構造一個新的jQuery對象,因為定義了jQuery.methodName的庫本身在更底層使用jQuery.fn.methodName驅動的。
然而由于并不是所有jQuery的方法都有相應的單節點功能,Padolsey根據這個創意設計了jQuery.single工具。 這里的創意是一個單獨的jQuery對象會被被創建出來并且用于每一次對jQuery.single的調用(有意義的是僅有一個jQuery對象會被創建出來)。對于此的實現可以在下面看到,而且由于我們將來自多個可能的對象的數據整合到一個更加集中的單一結構中,技術上講,它也是一個享元。
~~~
jQuery.single = (function( o ){
var collection = jQuery([1]);
return function( element ) {
// Give collection the element:
collection[0] = element;
// Return the collection:
return collection;
};
});
~~~
對于這個的帶有調用鏈的動作的示例如下:
~~~
$( "div" ).on( "click", function () {
var html = jQuery.single( this ).next().html();
console.log( html );
});
~~~
> 注意:盡管我們可能相信通過簡單的緩存我們的jQuery代碼會提供出同等良好的性能收獲,但Padolsey聲稱$.single()仍然值得使用,且表現更好。那并不是說不使用任何的緩存,只要對這種方法的助益做到心里有數就行。想要對$.single有更加詳細的了解,建議你卻讀一讀Padolsey完整的文章。
- 前言
- 簡介
- 什么是設計模式?
- 設計模式的結構
- 編寫設計模式
- 反模式
- 設計模式的分類
- 設計模式分類概覽表
- JavaScript 設計模式
- 構造器模式
- 模塊化模式
- 暴露模塊模式
- 單例模式
- 觀察者模式
- 中介者模式
- 原型模式
- 命令模式
- 外觀模式
- 工廠模式
- Mixin 模式
- 裝飾模式
- 亨元(Flyweight)模式
- JavaScript MV* 模式
- MVC 模式
- MVP 模式
- MVVM 模式
- 最新的模塊化 JavaScript 設計模式
- AMD
- CommonJS
- ES Harmony
- JQuery 中的設計模式
- 組合模式
- 適配器模式
- 外觀模式
- 觀察者模式
- 迭代器模式
- 惰性初始模式
- 代理模式
- 建造者模式
- jQuery 插件的設計模式
- JavaScript 命名空間模式
- 總結
- 參考