JavaScript設計模式
- 2021 年 1 月 3 日
- 筆記
- javascript設計模式, js, 設計模式
設計模式簡介:
設計模式是可重用的用於解決軟體設計中一般問題的方案。設計模式如此讓人著迷,以至在任何程式語言中都有對其進行的探索。
其中一個原因是它可以讓我們站在巨人的肩膀上,獲得前人所有的經驗,保證我們以優雅的方式組織我們的程式碼,滿足我們解決問題所需要的條件。
設計模式同樣也為我們描述問題提供了通用的辭彙。這比我們通過程式碼來向別人傳達語法和語義性的描述更為方便。
下面介紹一些JavaScript里用到的設計模式:
1.構造器模式
在面向對象編程中,構造器是一個當新建對象的記憶體被分配後,用來初始化該對象的一個特殊函數。在JavaScript中幾乎所有的東西都是對象,我們經常會對對象的構造器十分感興趣。
對象構造器是被用來創建特殊類型的對象的,首先它要準備使用的對象,其次在對象初次被創建時,通過接收參數,構造器要用來對成員的屬性和方法進行賦值。
1.1創建對象
// 第一種方式 let obj = {}; // 第二種方式 let obj2 = Object.create( null ); // 第三種方式 let obj3 = new Object();
1.2設置對象的屬性和方法
// 1. 「點號」法 // 設置屬性 obj.firstKey = "Hello World"; // 獲取屬性 let key = obj.firstKey; // 2. 「方括弧」法 // 設置屬性 obj["firstKey"] = "Hello World"; // 獲取屬性 let key = newObject["firstKey"]; // 方法1和2的區別在於用方括弧的方式內可以寫表達式 // 3. Object.defineProperty方式 // 設置屬性 Object.defineProperty(obj, "firstKey", { value: "hello world",// 屬性的值,默認為undefined writable: true, // 是否可修改,默認為false enumerable: true,// 是否可枚舉(遍歷),默認為false configurable: true // 表示對象的屬性是否可以被刪除,以及除 value 和 writable 特性外的其他特性是否可以被修改。 }); // 如果上面的方式你感到難以閱讀,可以簡短的寫成下面這樣: let defineProp = function ( obj, key, value ){
let config = {}; config.value = value; Object.defineProperty( obj, key, config ); }; // 4. Object.defineProperties方式(同時設置多個屬性) // 設置屬性 Object.defineProperties( obj, { "firstKey": { value: "Hello World", writable: true }, "secondKey": { value: "Hello World2", writable: false } });
1.3創建構造器
Javascript不支援類的概念,但它有一種與對象一起工作的構造器函數。使用new關鍵字來調用該函數,我們可以告訴Javascript把這個函數當做一個構造器來用,它可以用自己所定義的成員來初始化一個對象。
在這個構造器內部,關鍵字this引用到剛被創建的對象。回到對象創建,一個基本的構造函數看起來像這樣:
function Car( model, year, miles ) { this.model = model; this.year = year; this.miles = miles; this.toString = function () { return this.model + " has done " + this.miles + " miles"; }; } // 使用: // 我們可以示例化一個Car let civic = new Car( "Honda Civic", 2009, 20000 ); let mondeo = new Car( "Ford Mondeo", 2010, 5000 ); // 打開瀏覽器控制台查看這些對象toString()方法的輸出值 // output of the toString() method being called on // these objects console.log( civic.toString() ); console.log( mondeo.toString() );
上面是簡單版本的構造器模式,但它還是有些問題。一個是難以繼承,另一個是每個Car構造函數創建的對象中,toString()之類的函數都被重新定義。這不是非常好,理想的情況是所有Car類型的對象都應該引用同一個函數。
在Javascript中函數有一個prototype的屬性。當我們調用Javascript的構造器創建一個對象時,構造函數prototype上的屬性對於所創建的對象來說都看見。照這樣,就可以創建多個訪問相同prototype的Car對象了。下面,我們來擴展一下原來的例子:
function Car( model, year, miles ) { this.model = model; this.year = year; this.miles = miles; } Car.prototype.toString = function () { return this.model + " has done " + this.miles + " miles"; }; // 使用: var civic = new Car( "Honda Civic", 2009, 20000 ); var mondeo = new Car( "Ford Mondeo", 2010, 5000 ); console.log( civic.toString() ); console.log( mondeo.toString() );
通過上面程式碼,單個toString()實例被所有的Car對象所共享了。
2.模組化模式
模組是任何健壯的應用程式體系結構不可或缺的一部分,特點是有助於保持應用項目的程式碼單元既能清晰地分離又有組織。
在JavaScript中,實現模組有幾個選項,他們包括:
- 模組化模式
- 對象表示法
- AMD模組
- CommonJS 模組
- ECMAScript Harmony 模組
2.1對象字面值
對象字面值不要求使用新的操作實例,但是不能夠在結構體開始使用,因為打開”{“可能被解釋為一個塊的開始。
let myModule = { myProperty: "someValue", // 對象字面值包含了屬性和方法(properties and methods). // 例如,我們可以定義一個模組配置進對象: myConfig: { useCaching: true, language: "en" }, // 非常基本的方法 myMethod: function () { console.log( "Where in the world is Paul Irish today?" ); }, // 輸出基於當前配置configuration的一個值 myMethod2: function () { console.log( "Caching is:" + ( this.myConfig.useCaching ) ? "enabled" : "disabled" ); }, // 重寫當前的配置(configuration) myMethod3: function( newConfig ) { if ( typeof newConfig === "object" ) { this.myConfig = newConfig; console.log( this.myConfig.language ); } } }; myModule.myMethod();// Where in the world is Paul Irish today? myModule.myMethod2();// enabled myModule.myMethod3({ language: "fr", useCaching: false });// fr
2.2模組化模式
模組化模式最初被定義為一種對傳統軟體工程中的類提供私有和公共封裝的方法。
在JavaScript中,模組化模式用來進一步模擬類的概念,通過這樣一種方式:我們可以在一個單一的對象中包含公共/私有的方法和變數,從而從全局範圍中屏蔽特定的部分。這個結果是可以減少我們的函數名稱與在頁面中其他腳本區域定義的函數名稱衝突的可能性。
模組模式使用閉包的方式來將”私有資訊”,狀態和組織結構封裝起來。提供了一種將公有和私有方法,變數封裝混合在一起的方式,這種方式防止內部資訊泄露到全局中,從而避免了和其它開發者介面發生沖圖的可能性。在這種模式下只有公有的API 會返回,其它將全部保留在閉包的私有空間中。
這種方法提供了一個比較清晰的解決方案,在只暴露一個介面供其它部分使用的情況下,將執行繁重任務的邏輯保護起來。這個模式非常類似於立即調用函數式表達式(IIFE-查看命名空間相關章節獲取更多資訊),但是這種模式返回的是對象,而立即調用函數表達式返回的是一個函數。
需要注意的是,在javascript事實上沒有一個顯式的真正意義上的”私有性”概念,因為與傳統語言不同,javascript沒有訪問修飾符。從技術上講,變數不能被聲明為公有的或者私有的,因此我們使用函數域的方式去模擬這個概念。在模組模式中,因為閉包的緣故,聲明的變數或者方法只在模組內部有效。在返回對象中定義的變數或者方法可以供任何人使用。
let testModule = (function () { let counter = 0; return { incrementCounter: function () { return counter++; }, resetCounter: function () { console.log( "counter value prior to reset: " + counter ); counter = 0; } }; })(); testModule.incrementCounter(); testModule.resetCounter();
在這裡我們看到,其它部分的程式碼不能直接訪問我們的incrementCounter() 或者 resetCounter()的值。counter變數被完全從全局域中隔離起來了,因此其表現的就像一個私有變數一樣,它的存在只局限於模組的閉包內部,因此只有兩個函數可以訪問counter。我們的方法是有名字空間限制的,因此在我們程式碼的測試部分,我們需要給所有函數調用前面加上模組的名字(例如”testModule”)。
當使用模組模式時,我們會發現通過使用簡單的模板,對於開始使用模組模式非常有用。下面是一個模板包含了命名空間,公共變數和私有變數。
let myNamespace = (function () { let myPrivateVar, myPrivateMethod; myPrivateVar = 0; myPrivateMethod = function( foo ) { console.log( foo ); }; return { myPublicVar: "foo", myPublicFunction: function( bar ) { myPrivateVar++; myPrivateMethod( bar ); } }; })();
看一下另外一個例子,下面我們看到一個使用這種模式實現的購物車。這個模組完全自包含在一個叫做basketModule 全局變數中。模組中的購物車數組是私有的,應用的其它部分不能直接讀取。只存在與模組的閉包中,因此只有可以訪問其域的方法可以訪問這個變數。
let basketModule = (function () { let basket = []; function doSomethingPrivate() { //... } function doSomethingElsePrivate() { //... } return { addItem: function( values ) { basket.push(values); }, getItemCount: function () { return basket.length; }, doSomething: doSomethingPrivate, getTotal: function () { let q = this.getItemCount(), p = 0; while (q--) { p += basket[q].price; } return p; } }; }());
上面的方法都處於basketModule 的名字空間中。
請注意在上面的basket模組中 域函數是如何在我們所有的函數中被封裝起來的,以及我們如何立即調用這個域函數,並且將返回值保存下來。這種方式有以下的優勢:
- 可以創建只能被我們模組訪問的私有函數。這些函數沒有暴露出來(只有一些API是暴露出來的),它們被認為是完全私有的。
- 當我們在一個調試器中,需要發現哪個函數拋出異常的時候,可以很容易的看到調用棧,因為這些函數是正常聲明的並且是命名的函數。
- 這種模式同樣可以讓我們在不同的情況下返回不同的函數。我見過有開發者使用這種技巧用於執行測試,目的是為了在他們的模組裡面針對IE專門提供一條程式碼路徑,但是現在我們也可以簡單的使用特徵檢測達到相同的目的。
2.3Import mixins(導入混合)
這個變體展示了如何將全局(例如 jQuery, Underscore)作為一個參數傳入模組的匿名函數。這種方式允許我們導入全局,並且按照我們的想法在本地為這些全局起一個別名。
let myModule = (function ( jQ, _ ) { function privateMethod1(){ jQ(".container").html("test"); } function privateMethod2(){ console.log( _.min([10, 5, 100, 2, 1000]) ); } return{ publicMethod: function(){ privateMethod1(); } }; }( jQuery, _ ));// 將JQ和lodash導入 myModule.publicMethod();
2.4Exports(導出)
這個變體允許我們聲明全局對象而不用使用它們。
let myModule = (function () { let module = {}, privateVariable = "Hello World"; function privateMethod() { // ... } module.publicProperty = "Foobar"; module.publicMethod = function () { console.log( privateVariable ); }; return module; }());
2.5其它框架特定的模組模式實現
Dojo:
Dojo提供了一個方便的方法 dojo.setObject() 來設置對象。這需要將以”.”符號為第一個參數的分隔符,如:myObj.parent.child 是指定義在”myOjb”內部的一個對象「parent」,它的一個屬性為”child”。使用setObject()方法允許我們設置children 的值,可以創建路徑傳遞過程中的任何對象即使這些它們根本不存在。
例如,如果我們聲明商店命名空間的對象basket.coreas,可以使用如下方式:
let store = window.store || {}; if ( !store["basket"] ) { store.basket = {}; } if ( !store.basket["core"] ) { store.basket.core = {}; } store.basket.core = { key:value, };
ExtJS:
// create namespace Ext.namespace("myNameSpace"); // create application myNameSpace.app = function () { // do NOT access DOM from here; elements don't exist yet // private variables let btn1, privVar1 = 11; // private functions let btn1Handler = function ( button, event ) { console.log( "privVar1=" + privVar1 ); console.log( "this.btn1Text=" + this.btn1Text ); }; // public space return { // public properties, e.g. strings to translate btn1Text: "Button 1", // public methods init: function () { if ( Ext.Ext2 ) { btn1 = new Ext.Button({ renderTo: "btn1-ct", text: this.btn1Text, handler: btn1Handler }); } else { btn1 = new Ext.Button( "btn1-ct", { text: this.btn1Text, handler: btn1Handler }); } } }; }();
jQuery:
因為jQuery編碼規範沒有規定插件如何實現模組模式,因此有很多種方式可以實現模組模式。Ben Cherry 之間提供一種方案,因為模組之間可能存在大量的共性,因此通過使用函數包裝器封裝模組的定義。
在下面的例子中,定義了一個library 函數,這個函數聲明了一個新的庫,並且在新的庫(例如 模組)創建的時候,自動將初始化函數綁定到document的ready上。
function library( module ) { $( function() { if ( module.init ) { module.init(); } }); return module; } let myLibrary = library(function () { return { init: function () { // module implementation } }; }());
優點:
既然我們已經看到單例模式很有用,為什麼還是使用模組模式呢?首先,對於有面向對象背景的開發者來講,至少從javascript語言上來講,模組模式相對於真正的封裝概念更清晰。
其次,模組模式支援私有數據-因此,在模組模式中,公共部分程式碼可以訪問私有數據,但是在模組外部,不能訪問類的私有部分(沒開玩笑!感謝David Engfer 的玩笑)。
缺點:
模組模式的缺點是因為我們採用不同的方式訪問公有和私有成員,因此當我們想要改變這些成員的可見性的時候,我們不得不在所有使用這些成員的地方修改程式碼。
我們也不能在對象之後添加的方法裡面訪問這些私有變數。也就是說,很多情況下,模組模式很有用,並且當使用正確的時候,潛在地可以改善我們程式碼的結構。
其它缺點包括不能為私有成員創建自動化的單元測試,以及在緊急修復bug時所帶來的額外的複雜性。根本沒有可能可以對私有成員打修補程式。相反地,我們必須覆蓋所有的使用存在bug私有成員的公共方法。開發者不能簡單的擴展私有成員,因此我們需要記得,私有成員並非它們表面上看上去那麼具有擴展性。
3.單例模式
單例模式之所以這麼叫,是因為它限制一個類只能有一個實例化對象。經典的實現方式是,創建一個類,這個類包含一個方法,這個方法在沒有對象存在的情況下,將會創建一個新的實例對象。如果對象存在,這個方法只是返回這個對象的引用。
在JavaScript語言中, 單例服務作為一個從全局空間的程式碼實現中隔離出來共享的資源空間是為了提供一個單獨的函數訪問指針。
我們能像這樣實現一個單例:
let mySingleton = (function () { // Instance stores a reference to the Singleton let instance; function init() { // 單例 // 私有方法和變數 function privateMethod(){ console.log( "I am private" ); } let privateVariable = "Im also private"; let privateRandomNumber = Math.random(); return { // 共有方法和變數 publicMethod: function () { console.log( "The public can see me!" ); }, publicProperty: "I am also public", getRandomNumber: function() { return privateRandomNumber; } }; }; return { // 如果存在獲取此單例實例,如果不存在創建一個單例實例 getInstance: function () { if ( !instance ) { instance = init(); } return instance; } }; })(); let myBadSingleton = (function () { // 存儲單例實例的引用 var instance; function init() { // 單例 let privateRandomNumber = Math.random(); return { getRandomNumber: function() { return privateRandomNumber; } }; }; return { // 總是創建一個新的實例 getInstance: function () { instance = init(); return instance; } }; })(); // 使用: let singleA = mySingleton.getInstance(); let singleB = mySingleton.getInstance(); console.log( singleA.getRandomNumber() === singleB.getRandomNumber() ); // true let badSingleA = myBadSingleton.getInstance(); let badSingleB = myBadSingleton.getInstance(); console.log( badSingleA.getRandomNumber() !== badSingleB.getRandomNumber() ); // true
創建一個全局訪問的單例實例 (通常通過 MySingleton.getInstance()) 因為我們不能(至少在靜態語言中) 直接調用 new MySingleton() 創建實例. 這在JavaScript語言中是不可能的。
在四人幫(GoF)的書裡面,單例模式的應用描述如下:
- 每個類只有一個實例,這個實例必須通過一個廣為人知的介面,來被客戶訪問。
- 子類如果要擴展這個唯一的實例,客戶可以不用修改程式碼就能使用這個擴展後的實例。
關於第二點,可以參考如下的實例,我們需要這樣編碼:
mySingleton.getInstance = function(){ if ( this._instance == null ) { if ( isFoo() ) { this._instance = new FooSingleton(); } else { this._instance = new BasicSingleton(); } } return this._instance; };
在這裡,getInstance 有點類似於工廠方法,我們不需要去更新每個訪問單例的程式碼。FooSingleton可以是BasicSinglton的子類,並且實現了相同的介面。
儘管單例模式有著合理的使用需求,但是通常當我們發現自己需要在javascript使用它的時候,這是一種訊號,表明我們可能需要去重新評估自己的設計。
這通常表明系統中的模組要麼緊耦合要麼邏輯過於分散在程式碼庫的多個部分。單例模式更難測試,因為可能有多種多樣的問題出現,例如隱藏的依賴關係,很難去創建多個實例,很難清理依賴關係,等等。
4.觀察者模式
觀察者模式是這樣一種設計模式:一個被稱作被觀察者的對象,維護一組被稱為觀察者的對象,這些對象依賴於被觀察者,被觀察者自動將自身的狀態的任何變化通知給它們。
當一個被觀察者需要將一些變化通知給觀察者的時候,它將採用廣播的方式,這條廣播可能包含特定於這條通知的一些數據。
當特定的觀察者不再需要接受來自於它所註冊的被觀察者的通知的時候,被觀察者可以將其從所維護的組中刪除。 在這裡提及一下設計模式現有的定義很有必要。這個定義是與所使用的語言無關的。通過這個定義,最終我們可以更深層次地了解到設計模式如何使用以及其優勢。在四人幫的《設計模式:可重用的面向對象軟體的元素》這本書中,是這樣定義觀察者模式的:
一個或者更多的觀察者對一個被觀察者的狀態感興趣,將自身的這種興趣通過附著自身的方式註冊在被觀察者身上。當被觀察者發生變化,而這種便可也是觀察者所關心的,就會產生一個通知,這個通知將會被送出去,最後將會調用每個觀察者的更新方法。當觀察者不在對被觀察者的狀態感興趣的時候,它們只需要簡單的將自身剝離即可。
我們現在可以通過實現一個觀察者模式來進一步擴展我們剛才所學到的東西。這個實現包含一下組件:
- 被觀察者:維護一組觀察者, 提供用於增加和移除觀察者的方法。
- 觀察者:提供一個更新介面,用於當被觀察者狀態變化時,得到通知。
- 具體的被觀察者:狀態變化時廣播通知給觀察者,保持具體的觀察者的資訊。
- 具體的觀察者:保持一個指向具體被觀察者的引用,實現一個更新介面,用於觀察,以便保證自身狀態總是和被觀察者狀態一致的。
首先,讓我們對被觀察者可能有的一組依賴其的觀察者進行建模:
function ObserverList(){ this.observerList = []; } ObserverList.prototype.Add = function( obj ){ return this.observerList.push( obj ); }; ObserverList.prototype.Empty = function(){ this.observerList = []; }; ObserverList.prototype.Count = function(){ return this.observerList.length; }; ObserverList.prototype.Get = function( index ){ if( index > -1 && index < this.observerList.length ){ return this.observerList[ index ]; } }; ObserverList.prototype.Insert = function( obj, index ){ let pointer = -1; if( index === 0 ){ this.observerList.unshift( obj ); pointer = index; }else if( index === this.observerList.length ){ this.observerList.push( obj ); pointer = index; } return pointer; }; ObserverList.prototype.IndexOf = function( obj, startIndex ){ let i = startIndex, pointer = -1; while( i < this.observerList.length ){ if( this.observerList[i] === obj ){ pointer = i; } i++; } return pointer; }; ObserverList.prototype.RemoveAt = function( index ){ if( index === 0 ){ this.observerList.shift(); }else if( index === this.observerList.length -1 ){ this.observerList.pop(); } }; // Extend an object with an extension function extend( extension, obj ){ for ( let key in extension ){ obj[key] = extension[key]; } }
接著,我們對被觀察者以及其增加,刪除,通知在觀察者列表中的觀察者的能力進行建模:
function Subject(){ this.observers = new ObserverList(); } Subject.prototype.AddObserver = function( observer ){ this.observers.Add( observer ); }; Subject.prototype.RemoveObserver = function( observer ){ this.observers.RemoveAt( this.observers.IndexOf( observer, 0 ) ); }; Subject.prototype.Notify = function( context ){ let observerCount = this.observers.Count(); for(let i=0; i < observerCount; i++){ this.observers.Get(i).Update( context ); } };
我們接著定義建立新的觀察者的一個框架。這裡的update 函數之後會被具體的行為覆蓋。
// The Observer function Observer(){ this.Update = function(){ // ... }; }
在我們的樣例應用裡面,我們使用上面的觀察者組件,現在我們定義:
- 一個按鈕,這個按鈕用於增加新的充當觀察者的選擇框到頁面上
- 一個控制用的選擇框 , 充當一個被觀察者,通知其它選擇框是否應該被選中
- 一個容器,用於放置新的選擇框
我們接著定義具體被觀察者和具體觀察者,用於給頁面增加新的觀察者,以及實現更新介面。通過查看下面的內聯的注釋,搞清楚在我們樣例中的這些組件是如何工作的。
HTML
<button id="addNewObserver">Add New Observer checkbox</button> <input id="mainCheckbox" type="checkbox"/> <div id="observersContainer"></div>
Javascript
// 我們DOM 元素的引用 let controlCheckbox = document.getElementById("mainCheckbox"), addBtn = document.getElementById( "addNewObserver" ), container = document.getElementById( "observersContainer" ); // 具體的被觀察者 //Subject 類擴展controlCheckbox 類 extend( new Subject(), controlCheckbox ); //點擊checkbox 將會觸發對觀察者的通知 controlCheckbox["onclick"] = new Function("controlCheckbox.Notify(controlCheckbox.checked)"); addBtn["onclick"] = AddNewObserver; // 具體的觀察者 function AddNewObserver(){ //建立一個新的用於增加的checkbox let check = document.createElement( "input" ); check.type = "checkbox"; // 使用Observer 類擴展checkbox extend( new Observer(), check ); // 使用訂製的Update函數重載 check.Update = function( value ){ this.checked = value; }; // 增加新的觀察者到我們主要的被觀察者的觀察者列表中 controlCheckbox.AddObserver( check ); // 將元素添加到容器的最後 container.appendChild( check ); }
在這個例子裡面,我們看到了如何實現和配置觀察者模式,了解了被觀察者,觀察者,具體被觀察者,具體觀察者的概念。
觀察者模式和發布/訂閱模式的不同
觀察者模式確實很有用,但是在javascript時間裡面,通常我們使用一種叫做發布/訂閱模式的變體來實現觀察者模式。這兩種模式很相似,但是也有一些值得注意的不同。
觀察者模式要求想要接受相關通知的觀察者必須到發起這個事件的被觀察者上註冊這個事件。
發布/訂閱模式使用一個主題/事件頻道,這個頻道處於想要獲取通知的訂閱者和發起事件的發布者之間。這個事件系統允許程式碼定義應用相關的事件,這個事件可以傳遞特殊的參數,參數中包含有訂閱者所需要的值。這種想法是為了避免訂閱者和發布者之間的依賴性。
這種和觀察者模式之間的不同,使訂閱者可以實現一個合適的事件處理函數,用於註冊和接受由發布者廣播的相關通知。
這裡給出一個關於如何使用發布者/訂閱者模式的例子,這個例子中完整地實現了功能強大的publish(), subscribe() 和 unsubscribe()。
// 一個非常簡單的郵件處理器 // 接受的消息的計數器 let mailCounter = 0; // 初始化一個訂閱者,這個訂閱者監聽名叫"inbox/newMessage" 的頻道 // 渲染新消息的粗略資訊 let subscriber1 = subscribe( "inbox/newMessage", function( topic, data ) { // 日誌記錄主題,用於調試 console.log( "A new message was received: ", topic ); // 使用來自於被觀察者的數據,用於給用戶展示一個消息的粗略資訊 $( ".messageSender" ).html( data.sender ); $( ".messagePreview" ).html( data.body ); }); // 這是另外一個訂閱者,使用相同的數據執行不同的任務 // 更細計數器,顯示當前來自於發布者的新資訊的數量 let subscriber2 = subscribe( "inbox/newMessage", function( topic, data ) { $('.newMessageCounter').html( mailCounter++ ); }); publish( "inbox/newMessage", [{ sender:"[email protected]", body: "Hey there! How are you doing today?" }]); // 在之後,我們可以讓我們的訂閱者通過下面的方式取消訂閱來自於新主題的通知 // unsubscribe( subscriber1, ); // unsubscribe( subscriber2 );
這個例子的更廣的意義是對松耦合的原則的一種推崇。不是一個對象直接調用另外一個對象的方法,而是通過訂閱另外一個對象的一個特定的任務或者活動,從而在這個任務或者活動出現的時候的得到通知。
優點
觀察者和發布/訂閱模式鼓勵人們認真考慮應用不同部分之間的關係,同時幫助我們找出這樣的層,該層中包含有直接的關係,這些關係可以通過一些列的觀察者和被觀察者來替換掉。這中方式可以有效地將一個應用程式切割成小塊,這些小塊耦合度低,從而改善程式碼的管理,以及用於潛在的程式碼復用。
使用觀察者模式更深層次的動機是,當我們需要維護相關對象的一致性的時候,我們可以避免對象之間的緊密耦合。例如,一個對象可以通知另外一個對象,而不需要知道這個對象的資訊。
兩種模式下,觀察者和被觀察者之間都可以存在動態關係。這提供很好的靈活性,而當我們的應用中不同的部分之間緊密耦合的時候,是很難實現這種靈活性的。
儘管這些模式並不是萬能的靈丹妙藥,這些模式仍然是作為最好的設計松耦合系統的工具之一,因此在任何的JavaScript 開發者的工具箱裡面,都應該有這樣一個重要的工具。
缺點
事實上,這些模式的一些問題實際上正是來自於它們所帶來的一些好處。在發布/訂閱模式中,將發布者共訂閱者上解耦,將會在一些情況下,導致很難確保我們應用中的特定部分按照我們預期的那樣正常工作。
例如,發布者可以假設有一個或者多個訂閱者正在監聽它們。比如我們基於這樣的假設,在某些應用處理過程中來記錄或者輸出錯誤日誌。如果訂閱者執行日誌功能崩潰了(或者因為某些原因不能正常工作),因為系統本身的解耦本質,發布者沒有辦法感知到這些事情。
另外一個這種模式的缺點是,訂閱者對彼此之間存在沒有感知,對切換髮布者的代價無從得知。因為訂閱者和發布者之間的動態關係,更新依賴也很能去追蹤。
讓我們看一下最小的一個版本的發布/訂閱模式實現。這個實現展示了發布,訂閱的核心概念,以及如何取消訂閱。
let pubsub = {}; (function(q) { let topics = {}, subUid = -1; q.publish = function( topic, args ) { if ( !topics[topic] ) { return false; } let subscribers = topics[topic], len = subscribers ? subscribers.length : 0; while (len--) { subscribers[len].func( topic, args ); } return this; }; q.subscribe = function( topic, func ) { if (!topics[topic]) { topics[topic] = []; } let token = ( ++subUid ).toString(); topics[topic].push({ token: token, func: func }); return token; }; q.unsubscribe = function( token ) { for ( let m in topics ) { if ( topics[m] ) { for ( let i = 0, j = topics[m].length; i < j; i++ ) { if ( topics[m][i].token === token) { topics[m].splice( i, 1 ); return token; } } } } return this; }; }( pubsub ));
我們現在可以使用發布實例和訂閱感興趣的事件,例如:
let messageLogger = function ( topics, data ) { console.log( "Logging: " + topics + ": " + data ); }; let subscription = pubsub.subscribe( "inbox/newMessage", messageLogger ); pubsub.publish( "inbox/newMessage", "hello world!" ); // or pubsub.publish( "inbox/newMessage", ["test", "a", "b", "c"] ); // or pubsub.publish( "inbox/newMessage", { sender: "[email protected]", body: "Hey again!" }); // We cab also unsubscribe if we no longer wish for our subscribers // to be notified // pubsub.unsubscribe( subscription ); pubsub.publish( "inbox/newMessage", "Hello! are you still there?" );
觀察者模式在應用設計中,解耦一系列不同的場景上非常有用,如果你沒有用過它,我推薦你嘗試一下今天提到的之前寫到的某個實現。這個模式是一個易於學習的模式,同時也是一個威力巨大的模式。
5.中介者模式
如果系統組件之間存在大量的直接關係,就可能是時候,使用一個中心的控制點,來讓不同的組件通過它來通訊。中介者通過將組件之間顯式的直接的引用替換成通過中心點來交互的方式,來做到松耦合。這樣可以幫助我們解耦,和改善組件的重用性。
在現實世界中,類似的系統就是,飛行控制系統。一個航站塔(中介者)處理哪個飛機可以起飛,哪個可以著陸,因為所有的通訊(監聽的通知或者廣播的通知)都是飛機和控制塔之間進行的,而不是飛機和飛機之間進行的。一個中央集權的控制中心是這個系統成功的關鍵,也正是中介者在軟體設計領域中所扮演的角色。
5.1基礎的實現
中間人模式的一種簡單的實現可以在下面找到,publish()和subscribe()方法都被暴露出來使用:
let mediator = (function(){ let topics = {}; let subscribe = function( topic, fn ){ if ( !topics[topic] ){ topics[topic] = []; } topics[topic].push( { context: this, callback: fn } ); return this; }; let publish = function( topic ){ let args; if ( !topics[topic] ){ return false; } args = Array.prototype.slice.call( arguments, 1 ); for ( let i = 0, l = topics[topic].length; i < l; i++ ) { let subscription = topics[topic][i]; subscription.callback.apply( subscription.context, args ); } return this; }; return { publish: publish, subscribe: subscribe, installTo: function( obj ){ obj.subscribe = subscribe; obj.publish = publish; } }; }());
優點 & 缺點
中間人模式最大的好處就是,它節約了對象或者組件之間的通訊信道,這些對象或者組件存在於從多對多到多對一的系統之中。由於解耦合水平的因素,添加新的發布或者訂閱者是相對容易的。
也許使用這個模式最大的缺點是它可以引入一個單點故障。在模組之間放置一個中間人也可能會造成性能損失,因為它們經常是間接地的進行通訊的。由於松耦合的特性,僅僅盯著廣播很難去確認系統是如何做出反應的。
這就是說,提醒我們自己解耦合的系統擁有許多其它的好處,是很有用的——如果我們的模組互相之間直接的進行通訊,對於模組的改變(例如:另一個模組拋出了異常)可以很容易的對我們系統的其它部分產生多米諾連鎖效應。這個問題在解耦合的系統中很少需要被考慮到。
在一天結束的時候,緊耦合會導致各種頭痛,這僅僅只是另外一種可選的解決方案,但是如果得到正確實現的話也能夠工作得很好。
6.原型模式
原型模式是指通過克隆的方式基於一個現有對象的模板創建對象的模式。
我們能夠將原型模式認作是基於原型的繼承中,我們創建作為其它對象原型的對象.原型對象自身被當做構造器創建的每一個對象的藍本高效的使用著.如果構造器函數使用的原型包含例如叫做name的屬性,那麼每一個通過同一個構造器創建的對象都將擁有這個相同的屬性。
我們可以在下面的示例中看到對這個的展示:
let myCar = { name: "Ford Escort", drive: function () { console.log( "Weeee. I'm driving!" ); }, panic: function () { console.log( "Wait. How do you stop this thing?" ); } }; let yourCar = Object.create( myCar ); console.log( yourCar.name );// Ford Escort
Object.create也允許我們簡單的繼承先進的概念,比如對象能夠直接繼承自其它對象,這種不同的繼承.我們早先也看到Object.create允許我們使用 供應的第二個參數來初始化對象屬性。例如:
let vehicle = { getModel: function () { console.log( "The model of this vehicle is.." + this.model ); } }; let car = Object.create(vehicle, { "id": { value: "1", // writable:false, configurable:false by default enumerable: true }, "model": { value: "Ford", enumerable: true } });
這裡的屬性可以被Object.create的第二個參數來初始化,使用一種類似於Object.defineProperties和Object.defineProperties方法所使用語法的對象字面值。
在枚舉對象的屬性,和在一個hasOwnProperty()檢查中封裝循環的內容時,原型關係會造成麻煩,這一事實是值得我們關注的。
如果我們希望在不直接使用Object.create的前提下實現原型模式,我們可以像下面這樣,按照上面的示例,模擬這一模式:
let vehiclePrototype = { init: function ( carModel ) { this.model = carModel; }, getModel: function () { console.log( "The model of this vehicle is.." + this.model); } }; function vehicle( model ) { function F() {}; F.prototype = vehiclePrototype; let f = new F(); f.init( model ); return f; } let car = vehicle( "Ford Escort" ); car.getModel();
注意:這種可選的方式不允許用戶使用相同的方式定義只讀的屬性(因為如果不小心的話vehicle原型可能會被改變)。
原型模式的最後一種可選實現可以像下面這樣:
let beget = (function () { function F() {} return function ( proto ) { F.prototype = proto; return new F(); }; })();
7.命令模式
命名模式的目標是將方法的調用,請求或者操作封裝到一個單獨的對象中,給我們酌情執行同時參數化和傳遞方法調用的能力.另外,它使得我們能將對象從實現了行為的對象對這些行為的調用進行解耦,為我們帶來了換出具體的對象這一更深程度的整體靈活性。
具體類是對基於類的程式語言的最好解釋,並且同抽象類的理念聯繫緊密.抽象類定義了一個介面,但並不需要提供對它的所有成員函數的實現.它扮演著驅動其它類的基類角色.被驅動類實現了缺失的函數而被稱為具體類. 命令模式背後的一般理念是為我們提供了從任何執行中的命令中分離出發出命令的責任,取而代之將這一責任委託給其它的對象。
實現明智簡單的命令對象,將一個行為和對象對調用這個行為的需求都綁定到了一起.它們始終都包含一個執行操作(比如run()或者execute()).所有帶有相同介面的命令對象能夠被簡單地根據需要調換,這被認為是命令模式的更大的好處之一。
為了展示命令模式,我們創建一個簡單的汽車購買服務:
(function(){ let CarManager = { requestInfo: function( model, id ){ return "The information for " + model + " with ID " + id + " is foobar"; }, buyVehicle: function( model, id ){ return "You have successfully purchased Item " + id + ", a " + model; }, arrangeViewing: function( model, id ){ return "You have successfully booked a viewing of " + model + " ( " + id + " ) "; } }; })();
看一看上面的這段程式碼,它也許是通過直接訪問對象來瑣碎的調用我們CarManager的方法。在技術上我們也許都會都會對這個沒有任何失誤達成諒解.它是完全有效的Javascript然而也會有情況不利的情況。
例如,想像如果CarManager的核心API會發生改變的這種情況.這可能需要所有直接訪問這些方法的對象也跟著被修改.這可以被看成是一種耦合,明顯違背了OOP方法學盡量實現松耦合的理念.取而代之,我們可以通過更深入的抽象這些API來解決這個問題。
現在讓我們來擴展我們的CarManager,以便我們這個命令模式的應用程式得到接下來的這種效果:接受任何可以在CarManager對象上面執行的方法,傳送任何可以被使用到的數據,如Car模型和ID。
這裡是我們希望能夠實現的樣子:
CarManager.execute( "buyVehicle", "Ford Escort", "453543" );
按照這種結構,我們現在應該像下面這樣,添加一個對於”CarManager.execute()”方法的定義:
CarManager.execute = function ( name ) { return CarManager[name] && CarManager[name].apply( CarManager, [].slice.call(arguments, 1) ); };
最終我們的調用如下所示:
CarManager.execute( "arrangeViewing", "Ferrari", "14523" ); CarManager.execute( "requestInfo", "Ford Mondeo", "54323" ); CarManager.execute( "requestInfo", "Ford Escort", "34232" ); CarManager.execute( "buyVehicle", "Ford Escort", "34232" );
8.外觀模式
當我們提出一個門面,我們要向這個世界展現的是一個外觀,這一外觀可能藏匿著一種非常與眾不同的真實。這就是我們即將要回顧的模式背後的靈感——門面模式。這一模式提供了面向一種更大型的程式碼體提供了一個的更高級別的舒適的介面,隱藏了其真正的潛在複雜性。把這一模式想像成要是呈現給開發者簡化的API,一些總是會提升使用性能的東西。
為了在我們所學的基礎上進行構建,門面模式同時需要簡化一個類的介面,和把類同使用它的程式碼解耦。這給予了我們使用一種方式直接同子系統交互的能力,這一方式有時候會比直接訪問子系統更加不容易出錯。門面的優勢包括易用,還有常常實現起這個模式來只是一小段路,不費力。
讓我們通過實踐來看看這個模式。這是一個沒有經過優化的程式碼示例,但是這裡我們使用了一個門面來簡化跨瀏覽器事件監聽的介面。我們創建了一個公共的方法來實現,此方法能夠被用在檢查特性的存在的程式碼中,以便這段程式碼能夠提供一種安全和跨瀏覽器兼容方案。
let addMyEvent = function( el,ev,fn ){ if( el.addEventListener ){ el.addEventListener( ev,fn, false ); }else if(el.attachEvent){ el.attachEvent( "on" + ev, fn ); }else{ el["on" + ev] = fn; } };
門面不僅僅只被用在它們自己身上,它們也能夠被用來同其它的模式諸如模組模式進行集成。如我們在下面所看到的,我們模組模式的實體包含許多被定義為私有的方法。門面則被用來提供訪問這些方法的更加簡單的API:
let module = (function() { let _private = { i:5, get : function() { console.log( "current value:" + this.i); }, set : function( val ) { this.i = val; }, run : function() { console.log( "running" ); }, jump: function(){ console.log( "jumping" ); } }; return { facade : function( args ) { _private.set(args.val); _private.get(); if ( args.run ) { _private.run(); } } }; }()); module.facade( {run: true, val:10} );// "current value: 10" and "running"
在這個示例中,調用module.facade()將會觸發一堆模組中的私有方法。但再一次,用戶並不需要關心這些。我們已經使得對用戶而言不需要擔心實現級別的細節就能消受一種特性。
9.工廠模式
工廠模式是另外一種關注對象創建概念的創建模式。它的領域中同其它模式的不同之處在於它並沒有明確要求我們使用一個構造器。取而代之,一個工廠能提供一個創建對象的公共介面,我們可以在其中指定我們希望被創建的工廠對象的類型。
下面我們通過使用構造器模式邏輯來定義汽車。這個例子展示了Vehicle 工廠可以使用工廠模式來實現。
function Car( options ) { this.doors = options.doors || 4; this.state = options.state || "brand new"; this.color = options.color || "silver"; } function Truck( options){ this.state = options.state || "used"; this.wheelSize = options.wheelSize || "large"; this.color = options.color || "blue"; } function VehicleFactory() {} VehicleFactory.prototype.vehicleClass = Car; VehicleFactory.prototype.createVehicle = function ( options ) { if( options.vehicleType === "car" ){ this.vehicleClass = Car; }else{ this.vehicleClass = Truck; } return new this.vehicleClass( options ); }; let carFactory = new VehicleFactory(); let car = carFactory.createVehicle( { vehicleType: "car", color: "yellow", doors: 6 } ); console.log( car );
何時使用工廠模式
當被應用到下面的場景中時,工廠模式特別有用:
- 當我們的對象或者組件設置涉及到高程度級別的複雜度時。
- 當我們需要根據我們所在的環境方便的生成不同對象的實體時。
- 當我們在許多共享同一個屬性的許多小型對象或組件上工作時。
- 當帶有其它僅僅需要滿足一種API約定(又名鴨式類型)的對象的組合對象工作時.這對於解耦來說是有用的。
何時不要去使用工廠模式
當被應用到錯誤的問題類型上時,這一模式會給應用程式引入大量不必要的複雜性.除非為創建對象提供一個介面是我們編寫的庫或者框架的一個設計上目標,否則我會建議使用明確的構造器,以避免不必要的開銷。
由於對象的創建過程被高效的抽象在一個介面後面的事實,這也會給依賴於這個過程可能會有多複雜的單元測試帶來問題。
抽象工廠
了解抽象工廠模式也是非常實用的,它的目標是以一個通用的目標將一組獨立的工廠進行封裝.它將一堆對象的實現細節從它們的一般用例中分離。
抽象工廠應該被用在一種必須從其創建或生成對象的方式處獨立,或者需要同多種類型的對象一起工作,這樣的系統中。
簡單且容易理解的例子就是一個發動機工廠,它定義了獲取或者註冊發動機類型的方式.抽象工廠會被命名為AbstractVehicleFactory.抽象工廠將允許像”car”或者”truck”的發動機類型的定義,並且構造工廠將僅實現滿足發動機合約的類.(例如:Vehicle.prototype.driven和Vehicle.prototype.breakDown)。
let AbstractVehicleFactory = (function () { let types = {}; return { getVehicle: function ( type, customizations ) { var Vehicle = types[type]; return (Vehicle ? new Vehicle(customizations) : null); }, registerVehicle: function ( type, Vehicle ) { let proto = Vehicle.prototype; // only register classes that fulfill the vehicle contract if ( proto.drive && proto.breakDown ) { types[type] = Vehicle; } return AbstractVehicleFactory; } }; })(); AbstractVehicleFactory.registerVehicle( "car", Car ); AbstractVehicleFactory.registerVehicle( "truck", Truck ); let car = AbstractVehicleFactory.getVehicle( "car" , { color: "lime green", state: "like new" } ); let truck = AbstractVehicleFactory.getVehicle( "truck" , { wheelSize: "medium", color: "neon yellow" } );
10.Mixin 模式
mixin模式指一些提供能夠被一個或者一組子類簡單繼承功能的類,意在重用其功能。
子類劃分
子類劃分是一個參考了為一個新對象繼承來自一個基類或者超類對象的屬性的術語.在傳統的面向對象編程中,類B能夠從另外一個類A處擴展.這裡我們將A看做是超類,而將B看做是A的子類.如此,所有B的實體都從A處繼承了其A的方法.然而B仍然能夠定義它自己的方法,包括那些重載的原本在A中的定義的方法。
B是否應該調用已經被重載的A中的方法,我們將這個引述為方法鏈.B是否應該調用A(超類)的構造器,我們將這稱為構造器鏈。
為了演示子類劃分,首先我們需要一個能夠創建自身新實體的基對象。
let Person = function( firstName , lastName ){ this.firstName = firstName; this.lastName = lastName; this.gender = "male"; };
接下來,我們將制定一個新的類(對象),它是一個現有的Person對象的子類.讓我們想像我們想要加入一個不同屬性用來分辨一個Person和一個繼承了Person”超類”屬性的Superhero.由於超級英雄分享了一般人類許多共有的特徵(例如:name,gender),因此這應該很有希望充分展示出子類劃分是如何工作的。
let clark = new Person( "Clark" , "Kent" ); let Superhero = function( firstName, lastName , powers ){ Person.call( this, firstName, lastName ); this.powers = powers; }; SuperHero.prototype = Object.create( Person.prototype ); let superman = new Superhero( "Clark" ,"Kent" , ["flight","heat-vision"] ); console.log( superman );
Superhero構造器創建了一個自Peroson下降的對象。這種類型的對象擁有鏈中位於它之上的對象的屬性,而且如果我們在Person對象中設置了默認的值,Superhero能夠使用特定於它的對象的值覆蓋任何繼承的值。
Mixin(織入目標類)
在Javascript中,我們會將從Mixin繼承看作是通過擴展收集功能的一種途徑.我們定義的每一個新的對象都有一個原型,從其中它可以繼承更多的屬性.原型可以從其他對象繼承而來,但是更重要的是,能夠為任意數量的對象定義屬性.我們可以利用這一事實來促進功能重用。
Mix允許對象以最小量的複雜性從它們那裡借用(或者說繼承)功能.作為一種利用Javascript對象原型工作得很好的模式,它為我們提供了從不止一個Mix處分享功能的相當靈活,但比多繼承有效得多得多的方式。
它們可以被看做是其屬性和方法可以很容易的在其它大量對象原型共享的對象.想像一下我們定義了一個在一個標準對象字面量中含有實用功能的Mixin,如下所示:
let myMixins = { moveUp: function(){ console.log( "move up" ); }, moveDown: function(){ console.log( "move down" ); }, stop: function(){ console.log( "stop! in the name of love!" ); } };
然後我們可以方便的擴展現有構造器功能的原型,使其包含這種使用一個 如下面的score.js_.extends()方法輔助器的行為:
function carAnimator(){ this.moveLeft = function(){ console.log( "move left" ); }; } function personAnimator(){ this.moveRandomly = function(){ /*..*/ }; } _.extend( carAnimator.prototype, myMixins ); _.extend( personAnimator.prototype, myMixins ); let myAnimator = new carAnimator(); myAnimator.moveLeft(); myAnimator.moveDown(); myAnimator.stop();
如我們所見,這允許我們將通用的行為輕易的”混”入相當普通對象構造器中。
在接下來的示例中,我們有兩個構造器:一個Car和一個Mixin.我們將要做的是靜Car參數化(另外一種說法是擴展),以便它能夠繼承Mixin中的特定方法,名叫driveForwar()和driveBackward().這一次我們不會使用Underscore.js。
取而代之,這個示例將演示如何將一個構造器參數化,以便在無需重複每一個構造器函數過程的前提下包含其功能。
let Car = function ( settings ) { this.model = settings.model || "no model provided"; this.color = settings.color || "no colour provided"; }; // Mixin let Mixin = function () {}; Mixin.prototype = { driveForward: function () { console.log( "drive forward" ); }, driveBackward: function () { console.log( "drive backward" ); }, driveSideways: function () { console.log( "drive sideways" ); } }; function augment( receivingClass, givingClass ) { if ( arguments[2] ) { for ( var i = 2, len = arguments.length; i < len; i++ ) { receivingClass.prototype[arguments[i]] = givingClass.prototype[arguments[i]]; } }else { for ( let methodName in givingClass.prototype ) { if ( !Object.hasOwnProperty(receivingClass.prototype, methodName) ) { receivingClass.prototype[methodName] = givingClass.prototype[methodName]; } } } } augment( Car, Mixin, "driveForward", "driveBackward" ); let myCar = new Car({ model: "Ford Escort", color: "blue" }); myCar.driveForward(); myCar.driveBackward(); augment( Car, Mixin ); let mySportsCar = new Car({ model: "Porsche", color: "red" }); mySportsCar.driveSideways();
優點 & 缺點
Mixin支援在一個系統中降解功能的重複性,增加功能的重用性.在一些應用程式也許需要在所有的對象實體共享行為的地方,我們能夠通過在一個Mixin中維護這個共享的功能,來很容易的避免任何重複,而因此專註於只實現我們系統中真正彼此不同的功能。
也就是說,對Mixin的副作用是值得商榷的.一些開發者感覺將功能注入到對象的原型中是一個壞點子,因為它會同時導致原型污染和一定程度上的對我們原有功能的不確定性.在大型的系統中,很可能是有這種情況的。
但是,強大的文檔對最大限度的減少對待功能中的混入源的迷惑是有幫助的,而且對於每一種模式而言,如果在實現過程中小心行事,我們應該是沒多大問題的。
11.裝飾器模式
裝飾器是旨在提升重用性能的一種結構性設計模式。同Mixin類似,它可以被看作是應用子類劃分的另外一種有價值的可選方案。
典型的裝飾器提供了向一個系統中現有的類動態添加行為的能力。其創意是裝飾本身並不關心類的基礎功能,而只是將它自身拷貝到超類之中。
裝飾器模式並不去深入依賴於對象是如何創建的,而是專註於擴展它們的功能這一問題上。不同於只依賴於原型繼承,我們在一個簡單的基礎對象上面逐步添加能夠提供附加功能的裝飾對象。它的想法是,不同於子類劃分,我們向一個基礎對象添加(裝飾)屬性或者方法,因此它會是更加輕巧的。
向Javascript中的對象添加新的屬性是一個非常直接了當的過程,因此將這一特定牢記於心,一個非常簡單的裝飾器可以實現如下:
示例1:帶有新功能的裝飾構造器
function vehicle( vehicleType ){ this.vehicleType = vehicleType || "car"; this.model = "default"; this.license = "00000-000"; } let testInstance = new vehicle( "car" ); console.log( testInstance );// vehicle: car, model:default, license: 00000-000 let truck = new vehicle( "truck" ); truck.setModel = function( modelName ){ this.model = modelName; }; truck.setColor = function( color ){ this.color = color; }; truck.setModel( "CAT" ); truck.setColor( "blue" ); console.log( truck );// vehicle:truck, model:CAT, color: blue let secondInstance = new vehicle( "car" ); console.log( secondInstance );// vehicle: car, model:default, license: 00000-000
示例2:帶有多個裝飾器的裝飾對象
function MacBook() { this.cost = function () { return 997; }; this.screenSize = function () { return 11.6; }; } function Memory( macbook ) { let v = macbook.cost(); macbook.cost = function() { return v + 75; }; } function Engraving( macbook ){ let v = macbook.cost(); macbook.cost = function(){ return v + 200; }; } function Insurance( macbook ){ let v = macbook.cost(); macbook.cost = function(){ return v + 250; }; } let mb = new MacBook(); Memory( mb ); Engraving( mb ); Insurance( mb ); console.log( mb.cost() );// 1522 console.log( mb.screenSize() );// 11.6
在上面的示例中,我們的裝飾器重載了超類對象MacBook()的 object.cost()函數,使其返回的Macbook的當前價格加上了被訂製後升級的價格。
這被看做是對原來的Macbook對象構造器方法的裝飾,它並沒有將其重寫(例如,screenSize()),我們所定義的Macbook的其它屬性也保持不變,完好無缺。
優點 & 缺點
因為它可以被透明的使用,並且也相當的靈活,因此開發者都挺樂意去使用這個模式——如我們所見,對象可以用新的行為封裝或者「裝飾」起來,而後繼續使用,並不用去擔心基礎的對象被改變。在一個更加廣泛的範圍內,這一模式也避免了我們去依賴大量子類來實現同樣的效果。
然而在實現這個模式時,也存在我們應該意識到的缺點。如果窮於管理,它也會由於引入了許多微小但是相似的對象到我們的命名空間中,從而顯著的使得我們的應用程式架構變得複雜起來。這裡所擔憂的是,除了漸漸變得難於管理,其他不能熟練使用這個模式的開發者也可能會有一段要掌握它被使用的理由的艱難時期。
足夠的注釋或者對模式的研究,對此應該有助益,而只要我們對在我們的應程式中的多大範圍內使用這一模式有所掌控的話,我們就能讓兩方面都得到改善。
12.亨元模式
享元模式是一個優化重複、緩慢和低效數據共享程式碼的經典結構化解決方案。它的目標是以相關對象儘可能多的共享數據,來減少應用程式中記憶體的使用(例如:應用程式的配置、狀態等)。
此模式最先由Paul Calder 和 Mark Linton在1990提出,並用拳擊等級中少於112磅體重的等級名稱來命名。享元(「Flyweight」英語中的輕量級)的名稱本身是從以幫以助我們完成減少重量(記憶體標記)為目標的重量等級推導出的。
實際應用中,輕量級的數據共享採集被多個對象使用的相似對象或數據結構,並將這些數據放置於單個的擴展對象中。我們可以把它傳遞給依靠這些數據的對象,而不是在他們每個上面都存儲一次。
使用享元
有兩種方法來使用享元。第一種是數據層,基於存儲在記憶體中的大量相同對象的數據共享的概念。第二種是DOM層,享元模式被作為事件管理中心,以避免將事件處理程式關聯到我們需要相同行為父容器的所有子節點上。 享元模式通常被更多的用於數據層,我們先來看看它。
享元和數據共享
對於這個應用程式而言,圍繞經典的享元模式有更多需要我們意識到的概念。享元模式中有一個兩種狀態的概念——內在和外在。內在資訊可能會被我們的對象中的內部方法所需要,它們絕對不可以作為功能被帶出。外在資訊則可以被移除或者放在外部存儲。
帶有相同內在數據的對象可以被一個單獨的共享對象所代替,它通過一個工廠方法被創建出來。這允許我們去顯著降低隱式數據的存儲數量。
個中的好處是我們能夠留心於已經被初始化的對象,讓只有不同於我們已經擁有的對象的內在狀態時,新的拷貝才會被創建。
我們使用一個管理器來處理外在狀態。如何實現可以有所不同,但針對此的一種方法就是讓管理器對象包含一個存儲外在狀態以及它們所屬的享元對象的中心資料庫。
經典的享元實現
近幾年享元模式已經在Javascript中得到了深入的應用,我們會用到的許多實現方式其靈感來自於Java和C++的世界。
我們來看下來自維基百科的針對享元模式的 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介面,並且必須包含其介面的方法。
let CoffeeOrder = { // Interfaces serveCoffee:function(context){}, getFlavor:function(){} }; function CoffeeFlavor( newFlavor ){ let flavor = newFlavor; 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()); }; } } CoffeeFlavor.implementsFor( CoffeeOrder ); function CoffeeOrderContext( tableNumber ) { return{ getTable: function() { return tableNumber; } }; } function CoffeeFlavorFactory() { let flavors = {}, length = 0; return { getCoffeeFlavor: function (flavorName) { let flavor = flavors[flavorName]; if (flavor === undefined) { flavor = new CoffeeFlavor(flavorName); flavors[flavorName] = flavor; length++; } return flavor; }, getTotalCoffeeFlavorsMade: function () { return length; } }; } function testFlyweight(){ let flavors = new CoffeeFlavor(), tables = new CoffeeOrderContext(), ordersMade = 0, 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("total CoffeeFlavor objects made: " + flavorFactory.getTotalCoffeeFlavorsMade()); }
轉換程式碼為使用享元模式
接下來,讓我們通過實現一個管理一個圖書館中所有書籍的系統來繼續觀察享元。分析得知每一本書的重要元數據如下:
- ID
- 標題
- 作者
- 類型
- 總頁數
- 出版商ID
- ISBN
我們也將需要下面一些屬性,來跟蹤哪一個成員是被借出的一本特定的書,借出它們的日期,還有預計的歸還日期。
- 借出日期
- 借出的成員
- 規定歸還時間
- 可用性
let 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){ let currentDate = new Date(); return currentDate.getTime() > Date.parse( this.dueReturnDate ); } };
這對於最初小規模的藏書可能工作得還好,然而當圖書館擴充至每一本書的多個版本和可用的備份,這樣一個大型的庫存,我們會發現管理系統的運行隨著時間的推移會越來越慢。使用成千上萬的書籍對象可能會壓倒記憶體,而我們可以通過享元模式的提升來優化我們的系統。
現在我們可以像下面這樣將我們的數據分離成為內在和外在的狀態:同書籍對象(標題,版權歸屬)相關的數據是內在的,而借出數據(借出成員,規定歸還日期)則被看做是外在的。這實際上意味著對於每一種書籍屬性的組合僅需要一個書籍對象。這仍然具有相當大的數量,但相比之前已經得到大大的縮減了。
下面的書籍元數據組合的單一實體將在所有帶有一個特定標題的書籍拷貝中共享。
let 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; };
如我們所見,外在狀態已經被移除了。從圖書館借出所要做的一切都被轉移到一個管理器中,由於對象數據現在是分段的,工廠可以被用來做實例化。
一個基本工廠
現在讓我們定義一個非常基本的工廠。我們用它做的工作是,執行一個檢查來看看一本給定標題的書是不是之前已經在系統內創建過了;如果創建過了,我們就返回它 – 如果沒有,一本新書就會被創建並保存,使得以後可以訪問它。這確保了為每一條本質上唯一的數據,我們只創建了一份單一的拷貝:
let BookFactory = (function () { let existingBooks = {}, existingBook; return { createBook: function ( title, author, genre, pageCount, publisherID, ISBN ) { existingBook = existingBooks[ISBN]; if ( !!existingBook ) { return existingBook; } else { let book = new Book( title, author, genre, pageCount, publisherID, ISBN ); existingBooks[ISBN] = book; return book; } } }; });
管理外在狀態
下一步,我們需要將那些從Book對象中移除的狀態存儲到某一個地方——幸運的是一個管理器(我們會將其定義成一個單例)可以被用來封裝它們。書籍對象和借出這些書籍的圖書館成員的組合將被稱作書籍借出記錄。這些我們的管理器都將會存儲,並且也包含我們在對Book類進行享元優化期間剝離的同借出相關的邏輯。
let BookRecordManager = (function () { let bookRecordDatabase = {}; return { addBookRecord: function ( id, title, author, genre, pageCount, publisherID, ISBN, checkoutDate, checkoutMember, dueReturnDate, availability ) { let 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 ) { let 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 ) { let currentDate = new Date(); return currentDate.getTime() > Date.parse( bookRecordDatabase[bookID].dueReturnDate ); } }; });
這些改變的結果是所有從Book類中擷取的數據現在被存儲到了BookManager單例(BookDatabase)的一個屬性之中——與我們以前使用大量對象相比可以被認為是更加高效的東西。同書籍借出相關的方法也被設置在這裡,因為它們處理的數據是外在的而不內在的。
這個過程確實給我們最終的解決方法增加了一點點複雜性,然而同已經明智解決的數據性能問題相比,這只是一個小擔憂,如果我們有同一本書的30份拷貝,現在我們只需要存儲它一次就夠了。每一個函數也會佔用記憶體。使用享元模式這些函數只在一個地方存在(就是在管理器上),並且不是在每一個對象上面,這節約了記憶體上的使用。