設計模式簡介:
設計模式是可重用的用于解決軟體設計中一般問題的方案,設計模式如此讓人著迷,以至在任何編程語言中都有對其進行的探索,
其中一個原因是它可以讓我們站在巨人的肩膀上,獲得前人所有的經驗,保證我們以優雅的方式組織我們的代碼,滿足我們解決問題所需要的條件,
設計模式同樣也為我們描述問題提供了通用的詞匯,這比我們通過代碼來向別人傳達語法和語意性的描述更為方便,
下面介紹一些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份拷貝,現在我們只需要存盤它一次就夠了,每一個函式也會占用記憶體,使用享元模式這些函式只在一個地方存在(就是在管理器上),并且不是在每一個物件上面,這節約了記憶體上的使用,
轉載請註明出處,本文鏈接:https://www.uj5u.com/qiye/244187.html
標籤:其他
上一篇:JavaScript(五)-回圈
下一篇:JavaScript(五)-回圈
