JavaScript設(shè)計(jì)模式總匯

來源 |?http://www.fly63.com/article/detial/10063
設(shè)計(jì)模式簡介:
1、構(gòu)造器模式
1.1創(chuàng)建對象
// 第一種方式let obj = {};// 第二種方式let obj2 = Object.create( null );// 第三種方式let obj3 = new Object();
1.2設(shè)置對象的屬性和方法
// 1. “點(diǎn)號”法// 設(shè)置屬性obj.firstKey = "Hello World";// 獲取屬性let key = obj.firstKey;// 2. “方括號”法// 設(shè)置屬性obj["firstKey"] = "Hello World";// 獲取屬性let key = newObject["firstKey"];// 方法1和2的區(qū)別在于用方括號的方式內(nèi)可以寫表達(dá)式// 3. Object.defineProperty方式// 設(shè)置屬性Object.defineProperty(obj, "firstKey", {value: "hello world",// 屬性的值,默認(rèn)為undefinedwritable: true, // 是否可修改,默認(rèn)為falseenumerable: true,// 是否可枚舉(遍歷),默認(rèn)為falseconfigurable: true // 表示對象的屬性是否可以被刪除,以及除 value 和 writable 特性外的其他特性是否可以被修改。});// 如果上面的方式你感到難以閱讀,可以簡短的寫成下面這樣:let defineProp = function ( obj, key, value ){let config = {};config.value = value;Object.defineProperty( obj, key, config );};// 4. Object.defineProperties方式(同時(shí)設(shè)置多個(gè)屬性)// 設(shè)置屬性Object.defineProperties( obj, {"firstKey": {value: "Hello World",writable: true},"secondKey": {value: "Hello World2",writable: false}});
1.3創(chuàng)建構(gòu)造器
Javascript不支持類的概念,但它有一種與對象一起工作的構(gòu)造器函數(shù)。使用new關(guān)鍵字來調(diào)用該函數(shù),我們可以告訴Javascript把這個(gè)函數(shù)當(dāng)做一個(gè)構(gòu)造器來用,它可以用自己所定義的成員來初始化一個(gè)對象。
在這個(gè)構(gòu)造器內(nèi)部,關(guān)鍵字this引用到剛被創(chuàng)建的對象。回到對象創(chuàng)建,一個(gè)基本的構(gòu)造函數(shù)看起來像這樣:
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";};}// 使用:// 我們可以示例化一個(gè)Carlet 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 objectsconsole.log( civic.toString() );console.log( mondeo.toString() );
上面是簡單版本的構(gòu)造器模式,但它還是有些問題。一個(gè)是難以繼承,另一個(gè)是每個(gè)Car構(gòu)造函數(shù)創(chuàng)建的對象中,toString()之類的函數(shù)都被重新定義。這不是非常好,理想的情況是所有Car類型的對象都應(yīng)該引用同一個(gè)函數(shù)。?
在Javascript中函數(shù)有一個(gè)prototype的屬性。當(dāng)我們調(diào)用Javascript的構(gòu)造器創(chuàng)建一個(gè)對象時(shí),構(gòu)造函數(shù)prototype上的屬性對于所創(chuàng)建的對象來說都看見。照這樣,就可以創(chuàng)建多個(gè)訪問相同prototype的Car對象了。下面,我們來擴(kuò)展一下原來的例子:
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() );
通過上面代碼,單個(gè)toString()實(shí)例被所有的Car對象所共享了。
2、模塊化模式
模塊是任何健壯的應(yīng)用程序體系結(jié)構(gòu)不可或缺的一部分,特點(diǎn)是有助于保持應(yīng)用項(xiàng)目的代碼單元既能清晰地分離又有組織。
在JavaScript中,實(shí)現(xiàn)模塊有幾個(gè)選項(xiàng),他們包括:
模塊化模式
對象表示法
AMD模塊
Commonjs?模塊
ECMAScript Harmony 模塊
2.1對象字面值
對象字面值不要求使用新的操作實(shí)例,但是不能夠在結(jié)構(gòu)體開始使用,因?yàn)榇蜷_"{"可能被解釋為一個(gè)塊的開始。
let myModule = {myProperty: "someValue",// 對象字面值包含了屬性和方法(properties and methods).// 例如,我們可以定義一個(gè)模塊配置進(jìn)對象:myConfig: {useCaching: true,language: "en"},// 非常基本的方法myMethod: function () {console.log( "Where in the world is Paul Irish today?" );},// 輸出基于當(dāng)前配置configuration的一個(gè)值myMethod2: function () {console.log( "Caching is:" + ( this.myConfig.useCaching ) ? "enabled" : "disabled" );},// 重寫當(dāng)前的配置(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();// enabledmyModule.myMethod3({language: "fr",useCaching: false});// fr
2.2模塊化模式
模塊化模式最初被定義為一種對傳統(tǒng)軟件工程中的類提供私有和公共封裝的方法。
在JavaScript中,模塊化模式用來進(jìn)一步模擬類的概念,通過這樣一種方式:我們可以在一個(gè)單一的對象中包含公共/私有的方法和變量,從而從全局范圍中屏蔽特定的部分。
這個(gè)結(jié)果是可以減少我們的函數(shù)名稱與在頁面中其他腳本區(qū)域定義的函數(shù)名稱沖突的可能性。
模塊模式使用閉包的方式來將"私有信息",狀態(tài)和組織結(jié)構(gòu)封裝起來。提供了一種將公有和私有方法,變量封裝混合在一起的方式,這種方式防止內(nèi)部信息泄露到全局中,從而避免了和其它開發(fā)者接口發(fā)生沖圖的可能性。
在這種模式下只有公有的API 會(huì)返回,其它將全部保留在閉包的私有空間中。
這種方法提供了一個(gè)比較清晰的解決方案,在只暴露一個(gè)接口供其它部分使用的情況下,將執(zhí)行繁重任務(wù)的邏輯保護(hù)起來。這個(gè)模式非常類似于立即調(diào)用函數(shù)式表達(dá)式(IIFE-查看命名空間相關(guān)章節(jié)獲取更多信息),但是這種模式返回的是對象,而立即調(diào)用函數(shù)表達(dá)式返回的是一個(gè)函數(shù)。
需要注意的是,在javascript事實(shí)上沒有一個(gè)顯式的真正意義上的"私有性"概念,因?yàn)榕c傳統(tǒng)語言不同,javascript沒有訪問修飾符。從技術(shù)上講,變量不能被聲明為公有的或者私有的,因此我們使用函數(shù)域的方式去模擬這個(gè)概念。
在模塊模式中,因?yàn)殚]包的緣故,聲明的變量或者方法只在模塊內(nèi)部有效。在返回對象中定義的變量或者方法可以供任何人使用。
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變量被完全從全局域中隔離起來了,因此其表現(xiàn)的就像一個(gè)私有變量一樣,它的存在只局限于模塊的閉包內(nèi)部,因此只有兩個(gè)函數(shù)可以訪問counter。
我們的方法是有名字空間限制的,因此在我們代碼的測試部分,我們需要給所有函數(shù)調(diào)用前面加上模塊的名字(例如"testModule")。
當(dāng)使用模塊模式時(shí),我們會(huì)發(fā)現(xiàn)通過使用簡單的模板,對于開始使用模塊模式非常有用。下面是一個(gè)模板包含了命名空間,公共變量和私有變量。
let myNamespace = (function () {let myPrivateVar, myPrivateMethod;myPrivateVar = 0;myPrivateMethod = function( foo ) {console.log( foo );};return {myPublicVar: "foo",myPublicFunction: function( bar ) {myPrivateVar++;myPrivateMethod( bar );}};})();
看一下另外一個(gè)例子,下面我們看到一個(gè)使用這種模式實(shí)現(xiàn)的購物車。這個(gè)模塊完全自包含在一個(gè)叫做basketModule 全局變量中。
模塊中的購物車數(shù)組是私有的,應(yīng)用的其它部分不能直接讀取。只存在與模塊的閉包中,因此只有可以訪問其域的方法可以訪問這個(gè)變量。
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模塊中 域函數(shù)是如何在我們所有的函數(shù)中被封裝起來的,以及我們?nèi)绾瘟⒓凑{(diào)用這個(gè)域函數(shù),并且將返回值保存下來。這種方式有以下的優(yōu)勢:
可以創(chuàng)建只能被我們模塊訪問的私有函數(shù)。這些函數(shù)沒有暴露出來(只有一些API是暴露出來的),它們被認(rèn)為是完全私有的。
當(dāng)我們在一個(gè)調(diào)試器中,需要發(fā)現(xiàn)哪個(gè)函數(shù)拋出異常的時(shí)候,可以很容易的看到調(diào)用棧,因?yàn)檫@些函數(shù)是正常聲明的并且是命名的函數(shù)。
這種模式同樣可以讓我們在不同的情況下返回不同的函數(shù)。我見過有開發(fā)者使用這種技巧用于執(zhí)行測試,目的是為了在他們的模塊里面針對IE專門提供一條代碼路徑,但是現(xiàn)在我們也可以簡單的使用特征檢測達(dá)到相同的目的。
2.3Import mixins(導(dǎo)入混合)
這個(gè)變體展示了如何將全局(例如 jQuery, Underscore)作為一個(gè)參數(shù)傳入模塊的匿名函數(shù)。這種方式允許我們導(dǎo)入全局,并且按照我們的想法在本地為這些全局起一個(gè)別名。
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導(dǎo)入myModule.publicMethod();
2.4Exports(導(dǎo)出)
這個(gè)變體允許我們聲明全局對象而不用使用它們。
let myModule = (function () {let module = {},privateVariable = "Hello World";function privateMethod() {// ...}module.publicProperty = "Foobar";module.publicMethod = function () {console.log( privateVariable );};return module;}());
2.5其它框架特定的模塊模式實(shí)現(xiàn)
Dojo:
Dojo提供了一個(gè)方便的方法 dojo.setObject() 來設(shè)置對象。這需要將以"."符號為第一個(gè)參數(shù)的分隔符,如:myObj.parent.child 是指定義在"myOjb"內(nèi)部的一個(gè)對象“parent”,它的一個(gè)屬性為"child"。
使用setObject()方法允許我們設(shè)置children 的值,可以創(chuàng)建路徑傳遞過程中的任何對象即使這些它們根本不存在。
例如,如果我們聲明商店命名空間的對象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 namespaceExt.namespace("myNameSpace");// create applicationmyNameSpace.app = function () {// do NOT access DOM from here; elements don't exist yet// private variableslet btn1,privVar1 = 11;// private functionslet btn1Handler = function ( button, event ) {console.log( "privVar1=" + privVar1 );console.log( "this.btn1Text=" + this.btn1Text );};// public spacereturn {// public properties, e.g. strings to translatebtn1Text: "Button 1",// public methodsinit: 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:
因?yàn)閖Query編碼規(guī)范沒有規(guī)定插件如何實(shí)現(xiàn)模塊模式,因此有很多種方式可以實(shí)現(xiàn)模塊模式。Ben Cherry 之間提供一種方案,因?yàn)槟K之間可能存在大量的共性,因此通過使用函數(shù)包裝器封裝模塊的定義。
在下面的例子中,定義了一個(gè)library 函數(shù),這個(gè)函數(shù)聲明了一個(gè)新的庫,并且在新的庫(例如 模塊)創(chuàng)建的時(shí)候,自動(dòng)將初始化函數(shù)綁定到document的ready上。
function library( module ) {$( function() {if ( module.init ) {module.init();}});return module;}let myLibrary = library(function () {return {init: function () {// module implementation}};}());
優(yōu)點(diǎn):
既然我們已經(jīng)看到單例模式很有用,為什么還是使用模塊模式呢?首先,對于有面向?qū)ο蟊尘暗拈_發(fā)者來講,至少從javascript語言上來講,模塊模式相對于真正的封裝概念更清晰。
其次,模塊模式支持私有數(shù)據(jù)-因此,在模塊模式中,公共部分代碼可以訪問私有數(shù)據(jù),但是在模塊外部,不能訪問類的私有部分(沒開玩笑!感謝David Engfer 的玩笑)。
缺點(diǎn):
模塊模式的缺點(diǎn)是因?yàn)槲覀儾捎貌煌姆绞皆L問公有和私有成員,因此當(dāng)我們想要改變這些成員的可見性的時(shí)候,我們不得不在所有使用這些成員的地方修改代碼。
我們也不能在對象之后添加的方法里面訪問這些私有變量。也就是說,很多情況下,模塊模式很有用,并且當(dāng)使用正確的時(shí)候,潛在地可以改善我們代碼的結(jié)構(gòu)。
其它缺點(diǎn)包括不能為私有成員創(chuàng)建自動(dòng)化的單元測試,以及在緊急修復(fù)bug時(shí)所帶來的額外的復(fù)雜性。根本沒有可能可以對私有成員打補(bǔ)丁。
相反地,我們必須覆蓋所有的使用存在bug私有成員的公共方法。開發(fā)者不能簡單的擴(kuò)展私有成員,因此我們需要記得,私有成員并非它們表面上看上去那么具有擴(kuò)展性。
3、單例模式
單例模式之所以這么叫,是因?yàn)樗拗埔粋€(gè)類只能有一個(gè)實(shí)例化對象。經(jīng)典的實(shí)現(xiàn)方式是,創(chuàng)建一個(gè)類,這個(gè)類包含一個(gè)方法,這個(gè)方法在沒有對象存在的情況下,將會(huì)創(chuàng)建一個(gè)新的實(shí)例對象。如果對象存在,這個(gè)方法只是返回這個(gè)對象的引用。
在JavaScript語言中, 單例服務(wù)作為一個(gè)從全局空間的代碼實(shí)現(xiàn)中隔離出來共享的資源空間是為了提供一個(gè)單獨(dú)的函數(shù)訪問指針。
我們能像這樣實(shí)現(xiàn)一個(gè)單例:
let mySingleton = (function () {// Instance stores a reference to the Singletonlet 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 {// 如果存在獲取此單例實(shí)例,如果不存在創(chuàng)建一個(gè)單例實(shí)例getInstance: function () {if ( !instance ) {instance = init();}return instance;}};})();let myBadSingleton = (function () {// 存儲(chǔ)單例實(shí)例的引用var instance;function init() {// 單例let privateRandomNumber = Math.random();return {getRandomNumber: function() {return privateRandomNumber;}};};return {// 總是創(chuàng)建一個(gè)新的實(shí)例getInstance: function () {instance = init();return instance;}};})();// 使用:let singleA = mySingleton.getInstance();let singleB = mySingleton.getInstance();console.log( singleA.getRandomNumber() === singleB.getRandomNumber() ); // truelet badSingleA = myBadSingleton.getInstance();let badSingleB = myBadSingleton.getInstance();console.log( badSingleA.getRandomNumber() !== badSingleB.getRandomNumber() ); // true
創(chuàng)建一個(gè)全局訪問的單例實(shí)例 (通常通過 MySingleton.getInstance()) 因?yàn)槲覀儾荒?至少在靜態(tài)語言中) 直接調(diào)用 new MySingleton() 創(chuàng)建實(shí)例. 這在JavaScript語言中是不可能的。
在四人幫(GoF)的書里面,單例模式的應(yīng)用描述如下:
每個(gè)類只有一個(gè)實(shí)例,這個(gè)實(shí)例必須通過一個(gè)廣為人知的接口,來被客戶訪問。
子類如果要擴(kuò)展這個(gè)唯一的實(shí)例,客戶可以不用修改代碼就能使用這個(gè)擴(kuò)展后的實(shí)例。
關(guān)于第二點(diǎn),可以參考如下的實(shí)例,我們需要這樣編碼:
mySingleton.getInstance = function(){if ( this._instance == null ) {if ( isFoo() ) {this._instance = new FooSingleton();} else {this._instance = new BasicSingleton();}}return this._instance;};
在這里,getInstance 有點(diǎn)類似于工廠方法,我們不需要去更新每個(gè)訪問單例的代碼。FooSingleton可以是BasicSinglton的子類,并且實(shí)現(xiàn)了相同的接口。
盡管單例模式有著合理的使用需求,但是通常當(dāng)我們發(fā)現(xiàn)自己需要在javascript使用它的時(shí)候,這是一種信號,表明我們可能需要去重新評估自己的設(shè)計(jì)。
這通常表明系統(tǒng)中的模塊要么緊耦合要么邏輯過于分散在代碼庫的多個(gè)部分。單例模式更難測試,因?yàn)榭赡苡卸喾N多樣的問題出現(xiàn),例如隱藏的依賴關(guān)系,很難去創(chuàng)建多個(gè)實(shí)例,很難清理依賴關(guān)系,等等。
4、觀察者模式
觀察者模式是這樣一種設(shè)計(jì)模式:一個(gè)被稱作被觀察者的對象,維護(hù)一組被稱為觀察者的對象,這些對象依賴于被觀察者,被觀察者自動(dòng)將自身的狀態(tài)的任何變化通知給它們。
當(dāng)一個(gè)被觀察者需要將一些變化通知給觀察者的時(shí)候,它將采用廣播的方式,這條廣播可能包含特定于這條通知的一些數(shù)據(jù)。
當(dāng)特定的觀察者不再需要接受來自于它所注冊的被觀察者的通知的時(shí)候,被觀察者可以將其從所維護(hù)的組中刪除。在這里提及一下設(shè)計(jì)模式現(xiàn)有的定義很有必要。這個(gè)定義是與所使用的語言無關(guān)的。
通過這個(gè)定義,最終我們可以更深層次地了解到設(shè)計(jì)模式如何使用以及其優(yōu)勢。在四人幫的《設(shè)計(jì)模式:可重用的面向?qū)ο筌浖脑亍愤@本書中,是這樣定義觀察者模式的:
一個(gè)或者更多的觀察者對一個(gè)被觀察者的狀態(tài)感興趣,將自身的這種興趣通過附著自身的方式注冊在被觀察者身上。當(dāng)被觀察者發(fā)生變化,而這種便可也是觀察者所關(guān)心的,就會(huì)產(chǎn)生一個(gè)通知,這個(gè)通知將會(huì)被送出去,最后將會(huì)調(diào)用每個(gè)觀察者的更新方法。當(dāng)觀察者不在對被觀察者的狀態(tài)感興趣的時(shí)候,它們只需要簡單的將自身剝離即可。
我們現(xiàn)在可以通過實(shí)現(xiàn)一個(gè)觀察者模式來進(jìn)一步擴(kuò)展我們剛才所學(xué)到的東西。這個(gè)實(shí)現(xiàn)包含一下組件:
被觀察者:維護(hù)一組觀察者, 提供用于增加和移除觀察者的方法。
觀察者:提供一個(gè)更新接口,用于當(dāng)被觀察者狀態(tài)變化時(shí),得到通知。
具體的被觀察者:狀態(tài)變化時(shí)廣播通知給觀察者,保持具體的觀察者的信息。
具體的觀察者:保持一個(gè)指向具體被觀察者的引用,實(shí)現(xiàn)一個(gè)更新接口,用于觀察,以便保證自身狀態(tài)總是和被觀察者狀態(tài)一致的。
首先,讓我們對被觀察者可能有的一組依賴其的觀察者進(jìn)行建模:
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 extensionfunction extend( extension, obj ){for ( let key in extension ){obj[key] = extension[key];}}
接著,我們對被觀察者以及其增加,刪除,通知在觀察者列表中的觀察者的能力進(jìn)行建模:
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 );}};
我們接著定義建立新的觀察者的一個(gè)框架。這里的update 函數(shù)之后會(huì)被具體的行為覆蓋。
// The Observerfunction Observer(){this.Update = function(){// ...};}
在我們的樣例應(yīng)用里面,我們使用上面的觀察者組件,現(xiàn)在我們定義:
一個(gè)按鈕,這個(gè)按鈕用于增加新的充當(dāng)觀察者的選擇框到頁面上
一個(gè)控制用的選擇框 , 充當(dāng)一個(gè)被觀察者,通知其它選擇框是否應(yīng)該被選中
一個(gè)容器,用于放置新的選擇框
我們接著定義具體被觀察者和具體觀察者,用于給頁面增加新的觀察者,以及實(shí)現(xiàn)更新接口。通過查看下面的內(nèi)聯(lián)的注釋,搞清楚在我們樣例中的這些組件是如何工作的。
html
Javascript
// 我們DOM 元素的引用let controlCheckbox = document.getElementById("mainCheckbox"),addBtn = document.getElementById( "addNewObserver" ),container = document.getElementById( "observersContainer" );// 具體的被觀察者//Subject 類擴(kuò)展controlCheckbox 類extend( new Subject(), controlCheckbox );//點(diǎn)擊checkbox 將會(huì)觸發(fā)對觀察者的通知controlCheckbox["onclick"] = new Function("controlCheckbox.Notify(controlCheckbox.checked)");addBtn["onclick"] = AddNewObserver;// 具體的觀察者function AddNewObserver(){//建立一個(gè)新的用于增加的checkboxlet check = document.createElement( "input" );check.type = "checkbox";// 使用Observer 類擴(kuò)展checkboxextend( new Observer(), check );// 使用定制的Update函數(shù)重載check.Update = function( value ){this.checked = value;};// 增加新的觀察者到我們主要的被觀察者的觀察者列表中controlCheckbox.AddObserver( check );// 將元素添加到容器的最后container.appendChild( check );}
在這個(gè)例子里面,我們看到了如何實(shí)現(xiàn)和配置觀察者模式,了解了被觀察者,觀察者,具體被觀察者,具體觀察者的概念。
觀察者模式和發(fā)布/訂閱模式的不同
觀察者模式確實(shí)很有用,但是在javascript時(shí)間里面,通常我們使用一種叫做發(fā)布/訂閱模式的變體來實(shí)現(xiàn)觀察者模式。這兩種模式很相似,但是也有一些值得注意的不同。
觀察者模式要求想要接受相關(guān)通知的觀察者必須到發(fā)起這個(gè)事件的被觀察者上注冊這個(gè)事件。
發(fā)布/訂閱模式使用一個(gè)主題/事件頻道,這個(gè)頻道處于想要獲取通知的訂閱者和發(fā)起事件的發(fā)布者之間。
這個(gè)事件系統(tǒng)允許代碼定義應(yīng)用相關(guān)的事件,這個(gè)事件可以傳遞特殊的參數(shù),參數(shù)中包含有訂閱者所需要的值。這種想法是為了避免訂閱者和發(fā)布者之間的依賴性。
這種和觀察者模式之間的不同,使訂閱者可以實(shí)現(xiàn)一個(gè)合適的事件處理函數(shù),用于注冊和接受由發(fā)布者廣播的相關(guān)通知。
這里給出一個(gè)關(guān)于如何使用發(fā)布者/訂閱者模式的例子,這個(gè)例子中完整地實(shí)現(xiàn)了功能強(qiáng)大的publish(), subscribe() 和 unsubscribe()。
// 一個(gè)非常簡單的郵件處理器// 接受的消息的計(jì)數(shù)器let mailCounter = 0;// 初始化一個(gè)訂閱者,這個(gè)訂閱者監(jiān)聽名叫"inbox/newMessage" 的頻道// 渲染新消息的粗略信息let subscriber1 = subscribe( "inbox/newMessage", function( topic, data ) {// 日志記錄主題,用于調(diào)試console.log( "A new message was received: ", topic );// 使用來自于被觀察者的數(shù)據(jù),用于給用戶展示一個(gè)消息的粗略信息$( ".messageSender" ).html( data.sender );$( ".messagePreview" ).html( data.body );});// 這是另外一個(gè)訂閱者,使用相同的數(shù)據(jù)執(zhí)行不同的任務(wù)// 更細(xì)計(jì)數(shù)器,顯示當(dāng)前來自于發(fā)布者的新信息的數(shù)量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 );
這個(gè)例子的更廣的意義是對松耦合的原則的一種推崇。不是一個(gè)對象直接調(diào)用另外一個(gè)對象的方法,而是通過訂閱另外一個(gè)對象的一個(gè)特定的任務(wù)或者活動(dòng),從而在這個(gè)任務(wù)或者活動(dòng)出現(xiàn)的時(shí)候的得到通知。
優(yōu)點(diǎn)
觀察者和發(fā)布/訂閱模式鼓勵(lì)人們認(rèn)真考慮應(yīng)用不同部分之間的關(guān)系,同時(shí)幫助我們找出這樣的層,該層中包含有直接的關(guān)系,這些關(guān)系可以通過一些列的觀察者和被觀察者來替換掉。
這中方式可以有效地將一個(gè)應(yīng)用程序切割成小塊,這些小塊耦合度低,從而改善代碼的管理,以及用于潛在的代碼復(fù)用。
使用觀察者模式更深層次的動(dòng)機(jī)是,當(dāng)我們需要維護(hù)相關(guān)對象的一致性的時(shí)候,我們可以避免對象之間的緊密耦合。例如,一個(gè)對象可以通知另外一個(gè)對象,而不需要知道這個(gè)對象的信息。
兩種模式下,觀察者和被觀察者之間都可以存在動(dòng)態(tài)關(guān)系。這提供很好的靈活性,而當(dāng)我們的應(yīng)用中不同的部分之間緊密耦合的時(shí)候,是很難實(shí)現(xiàn)這種靈活性的。
盡管這些模式并不是萬能的靈丹妙藥,這些模式仍然是作為最好的設(shè)計(jì)松耦合系統(tǒng)的工具之一,因此在任何的JavaScript 開發(fā)者的工具箱里面,都應(yīng)該有這樣一個(gè)重要的工具。
缺點(diǎn)
事實(shí)上,這些模式的一些問題實(shí)際上正是來自于它們所帶來的一些好處。在發(fā)布/訂閱模式中,將發(fā)布者共訂閱者上解耦,將會(huì)在一些情況下,導(dǎo)致很難確保我們應(yīng)用中的特定部分按照我們預(yù)期的那樣正常工作。
例如,發(fā)布者可以假設(shè)有一個(gè)或者多個(gè)訂閱者正在監(jiān)聽它們。比如我們基于這樣的假設(shè),在某些應(yīng)用處理過程中來記錄或者輸出錯(cuò)誤日志。如果訂閱者執(zhí)行日志功能崩潰了(或者因?yàn)槟承┰虿荒苷9ぷ鳎驗(yàn)橄到y(tǒng)本身的解耦本質(zhì),發(fā)布者沒有辦法感知到這些事情。
另外一個(gè)這種模式的缺點(diǎn)是,訂閱者對彼此之間存在沒有感知,對切換發(fā)布者的代價(jià)無從得知。因?yàn)橛嗛喺吆桶l(fā)布者之間的動(dòng)態(tài)關(guān)系,更新依賴也很能去追蹤。
讓我們看一下最小的一個(gè)版本的發(fā)布/訂閱模式實(shí)現(xiàn)。這個(gè)實(shí)現(xiàn)展示了發(fā)布,訂閱的核心概念,以及如何取消訂閱。
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 ));
我們現(xiàn)在可以使用發(fā)布實(shí)例和訂閱感興趣的事件,例如:
let messageLogger = function ( topics, data ) {console.log( "Logging: " + topics + ": " + data );};let subscription = pubsub.subscribe( "inbox/newMessage", messageLogger );pubsub.publish( "inbox/newMessage", "hello world!" );// orpubsub.publish( "inbox/newMessage", ["test", "a", "b", "c"] );// orpubsub.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?" );
觀察者模式在應(yīng)用設(shè)計(jì)中,解耦一系列不同的場景上非常有用,如果你沒有用過它,我推薦你嘗試一下今天提到的之前寫到的某個(gè)實(shí)現(xiàn)。這個(gè)模式是一個(gè)易于學(xué)習(xí)的模式,同時(shí)也是一個(gè)威力巨大的模式。
5、中介者模式
如果系統(tǒng)組件之間存在大量的直接關(guān)系,就可能是時(shí)候,使用一個(gè)中心的控制點(diǎn),來讓不同的組件通過它來通信。中介者通過將組件之間顯式的直接的引用替換成通過中心點(diǎn)來交互的方式,來做到松耦合。這樣可以幫助我們解耦,和改善組件的重用性。
在現(xiàn)實(shí)世界中,類似的系統(tǒng)就是,飛行控制系統(tǒng)。一個(gè)航站塔(中介者)處理哪個(gè)飛機(jī)可以起飛,哪個(gè)可以著陸,因?yàn)樗械耐ㄐ牛ūO(jiān)聽的通知或者廣播的通知)都是飛機(jī)和控制塔之間進(jìn)行的,而不是飛機(jī)和飛機(jī)之間進(jìn)行的。一個(gè)中央集權(quán)的控制中心是這個(gè)系統(tǒng)成功的關(guān)鍵,也正是中介者在軟件設(shè)計(jì)領(lǐng)域中所扮演的角色。
5.1基礎(chǔ)的實(shí)現(xiàn)
中間人模式的一種簡單的實(shí)現(xiàn)可以在下面找到,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;}};}());
優(yōu)點(diǎn) & 缺點(diǎn)
中間人模式最大的好處就是,它節(jié)約了對象或者組件之間的通信信道,這些對象或者組件存在于從多對多到多對一的系統(tǒng)之中。由于解耦合水平的因素,添加新的發(fā)布或者訂閱者是相對容易的。
也許使用這個(gè)模式最大的缺點(diǎn)是它可以引入一個(gè)單點(diǎn)故障。在模塊之間放置一個(gè)中間人也可能會(huì)造成性能損失,因?yàn)樗鼈兘?jīng)常是間接地的進(jìn)行通信的。由于松耦合的特性,僅僅盯著廣播很難去確認(rèn)系統(tǒng)是如何做出反應(yīng)的。
這就是說,提醒我們自己解耦合的系統(tǒng)擁有許多其它的好處,是很有用的——如果我們的模塊互相之間直接的進(jìn)行通信,對于模塊的改變(例如:另一個(gè)模塊拋出了異常)可以很容易的對我們系統(tǒng)的其它部分產(chǎn)生多米諾連鎖效應(yīng)。這個(gè)問題在解耦合的系統(tǒng)中很少需要被考慮到。
在一天結(jié)束的時(shí)候,緊耦合會(huì)導(dǎo)致各種頭痛,這僅僅只是另外一種可選的解決方案,但是如果得到正確實(shí)現(xiàn)的話也能夠工作得很好。
6、原型模式
原型模式是指通過克隆的方式基于一個(gè)現(xiàn)有對象的模板創(chuàng)建對象的模式。
我們能夠?qū)⒃湍J秸J(rèn)作是基于原型的繼承中,我們創(chuàng)建作為其它對象原型的對象.原型對象自身被當(dāng)做構(gòu)造器創(chuàng)建的每一個(gè)對象的藍(lán)本高效的使用著.如果構(gòu)造器函數(shù)使用的原型包含例如叫做name的屬性,那么每一個(gè)通過同一個(gè)構(gòu)造器創(chuàng)建的對象都將擁有這個(gè)相同的屬性。
我們可以在下面的示例中看到對這個(gè)的展示:
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也允許我們簡單的繼承先進(jìn)的概念,比如對象能夠直接繼承自其它對象,這種不同的繼承.我們早先也看到Object.create允許我們使用 供應(yīng)的第二個(gè)參數(shù)來初始化對象屬性。例如:
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 defaultenumerable: true},"model": {value: "Ford",enumerable: true}});
這里的屬性可以被Object.create的第二個(gè)參數(shù)來初始化,使用一種類似于Object.defineProperties和Object.defineProperties方法所使用語法的對象字面值。
在枚舉對象的屬性,和在一個(gè)hasOwnProperty()檢查中封裝循環(huán)的內(nèi)容時(shí),原型關(guān)系會(huì)造成麻煩,這一事實(shí)是值得我們關(guān)注的。
如果我們希望在不直接使用Object.create的前提下實(shí)現(xiàn)原型模式,我們可以像下面這樣,按照上面的示例,模擬這一模式:
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();
注意:這種可選的方式不允許用戶使用相同的方式定義只讀的屬性(因?yàn)槿绻恍⌒牡脑抳ehicle原型可能會(huì)被改變)。
原型模式的最后一種可選實(shí)現(xiàn)可以像下面這樣:
let beget = (function () {function F() {}return function ( proto ) {F.prototype = proto;return new F();};})();
7、命令模式
命名模式的目標(biāo)是將方法的調(diào)用,請求或者操作封裝到一個(gè)單獨(dú)的對象中,給我們酌情執(zhí)行同時(shí)參數(shù)化和傳遞方法調(diào)用的能力.另外,它使得我們能將對象從實(shí)現(xiàn)了行為的對象對這些行為的調(diào)用進(jìn)行解耦,為我們帶來了換出具體的對象這一更深程度的整體靈活性。
具體類是對基于類的編程語言的最好解釋,并且同抽象類的理念聯(lián)系緊密。抽象類定義了一個(gè)接口,但并不需要提供對它的所有成員函數(shù)的實(shí)現(xiàn)。它扮演著驅(qū)動(dòng)其它類的基類角色.被驅(qū)動(dòng)類實(shí)現(xiàn)了缺失的函數(shù)而被稱為具體類.。命令模式背后的一般理念是為我們提供了從任何執(zhí)行中的命令中分離出發(fā)出命令的責(zé)任,取而代之將這一責(zé)任委托給其它的對象。
實(shí)現(xiàn)明智簡單的命令對象,將一個(gè)行為和對象對調(diào)用這個(gè)行為的需求都綁定到了一起.它們始終都包含一個(gè)執(zhí)行操作(比如run()或者execute()).所有帶有相同接口的命令對象能夠被簡單地根據(jù)需要調(diào)換,這被認(rèn)為是命令模式的更大的好處之一。
為了展示命令模式,我們創(chuàng)建一個(gè)簡單的汽車購買服務(wù):
(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 + " ) ";}};})();
看一看上面的這段代碼,它也許是通過直接訪問對象來瑣碎的調(diào)用我們CarManager的方法。在技術(shù)上我們也許都會(huì)都會(huì)對這個(gè)沒有任何失誤達(dá)成諒解.它是完全有效的Javascript然而也會(huì)有情況不利的情況。
例如,想象如果CarManager的核心API會(huì)發(fā)生改變的這種情況.這可能需要所有直接訪問這些方法的對象也跟著被修改.這可以被看成是一種耦合,明顯違背了OOP方法學(xué)盡量實(shí)現(xiàn)松耦合的理念.取而代之,我們可以通過更深入的抽象這些API來解決這個(gè)問題。
現(xiàn)在讓我們來擴(kuò)展我們的CarManager,以便我們這個(gè)命令模式的應(yīng)用程序得到接下來的這種效果:接受任何可以在CarManager對象上面執(zhí)行的方法,傳送任何可以被使用到的數(shù)據(jù),如Car模型和ID。
這里是我們希望能夠?qū)崿F(xiàn)的樣子:
CarManager.execute( "buyVehicle", "Ford Escort", "453543" );
按照這種結(jié)構(gòu),我們現(xiàn)在應(yīng)該像下面這樣,添加一個(gè)對于"CarManager.execute()"方法的定義:
CarManager.execute = function ( name ) {return CarManager[name] && CarManager[name].apply( CarManager, [].slice.call(arguments, 1) );};
最終我們的調(diào)用如下所示:
CarManager.execute( "arrangeViewing", "Ferrari", "14523" );CarManager.execute( "requestInfo", "Ford Mondeo", "54323" );CarManager.execute( "requestInfo", "Ford Escort", "34232" );CarManager.execute( "buyVehicle", "Ford Escort", "34232" );
8、外觀模式
當(dāng)我們提出一個(gè)門面,我們要向這個(gè)世界展現(xiàn)的是一個(gè)外觀,這一外觀可能藏匿著一種非常與眾不同的真實(shí)。這就是我們即將要回顧的模式背后的靈感——門面模式。
這一模式提供了面向一種更大型的代碼體提供了一個(gè)的更高級別的舒適的接口,隱藏了其真正的潛在復(fù)雜性。
把這一模式想象成要是呈現(xiàn)給開發(fā)者簡化的API,一些總是會(huì)提升使用性能的東西。
為了在我們所學(xué)的基礎(chǔ)上進(jìn)行構(gòu)建,門面模式同時(shí)需要簡化一個(gè)類的接口,和把類同使用它的代碼解耦。這給予了我們使用一種方式直接同子系統(tǒng)交互的能力,這一方式有時(shí)候會(huì)比直接訪問子系統(tǒng)更加不容易出錯(cuò)。
門面的優(yōu)勢包括易用,還有常常實(shí)現(xiàn)起這個(gè)模式來只是一小段路,不費(fèi)力。
讓我們通過實(shí)踐來看看這個(gè)模式。這是一個(gè)沒有經(jīng)過優(yōu)化的代碼示例,但是這里我們使用了一個(gè)門面來簡化跨瀏覽器事件監(jiān)聽的接口。我們創(chuàng)建了一個(gè)公共的方法來實(shí)現(xiàn),此方法能夠被用在檢查特性的存在的代碼中,以便這段代碼能夠提供一種安全和跨瀏覽器兼容方案。
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;}};
門面不僅僅只被用在它們自己身上,它們也能夠被用來同其它的模式諸如模塊模式進(jìn)行集成。如我們在下面所看到的,我們模塊模式的實(shí)體包含許多被定義為私有的方法。門面則被用來提供訪問這些方法的更加簡單的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"
在這個(gè)示例中,調(diào)用module.facade()將會(huì)觸發(fā)一堆模塊中的私有方法。但再一次,用戶并不需要關(guān)心這些。我們已經(jīng)使得對用戶而言不需要擔(dān)心實(shí)現(xiàn)級別的細(xì)節(jié)就能消受一種特性。
9、工廠模式
工廠模式是另外一種關(guān)注對象創(chuàng)建概念的創(chuàng)建模式。它的領(lǐng)域中同其它模式的不同之處在于它并沒有明確要求我們使用一個(gè)構(gòu)造器。
取而代之,一個(gè)工廠能提供一個(gè)創(chuàng)建對象的公共接口,我們可以在其中指定我們希望被創(chuàng)建的工廠對象的類型。
下面我們通過使用構(gòu)造器模式邏輯來定義汽車。這個(gè)例子展示了Vehicle 工廠可以使用工廠模式來實(shí)現(xiàn)。
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 );
何時(shí)使用工廠模式
當(dāng)被應(yīng)用到下面的場景中時(shí),工廠模式特別有用:
當(dāng)我們的對象或者組件設(shè)置涉及到高程度級別的復(fù)雜度時(shí)。
當(dāng)我們需要根據(jù)我們所在的環(huán)境方便的生成不同對象的實(shí)體時(shí)。
當(dāng)我們在許多共享同一個(gè)屬性的許多小型對象或組件上工作時(shí)。
當(dāng)帶有其它僅僅需要滿足一種API約定(又名鴨式類型)的對象的組合對象工作時(shí).這對于解耦來說是有用的。
何時(shí)不要去使用工廠模式
當(dāng)被應(yīng)用到錯(cuò)誤的問題類型上時(shí),這一模式會(huì)給應(yīng)用程序引入大量不必要的復(fù)雜性.除非為創(chuàng)建對象提供一個(gè)接口是我們編寫的庫或者框架的一個(gè)設(shè)計(jì)上目標(biāo),否則我會(huì)建議使用明確的構(gòu)造器,以避免不必要的開銷。
由于對象的創(chuàng)建過程被高效的抽象在一個(gè)接口后面的事實(shí),這也會(huì)給依賴于這個(gè)過程可能會(huì)有多復(fù)雜的單元測試帶來問題。
抽象工廠
了解抽象工廠模式也是非常實(shí)用的,它的目標(biāo)是以一個(gè)通用的目標(biāo)將一組獨(dú)立的工廠進(jìn)行封裝.它將一堆對象的實(shí)現(xiàn)細(xì)節(jié)從它們的一般用例中分離。
抽象工廠應(yīng)該被用在一種必須從其創(chuàng)建或生成對象的方式處獨(dú)立,或者需要同多種類型的對象一起工作,這樣的系統(tǒng)中。
簡單且容易理解的例子就是一個(gè)發(fā)動(dòng)機(jī)工廠,它定義了獲取或者注冊發(fā)動(dòng)機(jī)類型的方式。抽象工廠會(huì)被命名為AbstractVehicleFactory。抽象工廠將允許像"car"或者"truck"的發(fā)動(dòng)機(jī)類型的定義,并且構(gòu)造工廠將僅實(shí)現(xiàn)滿足發(fā)動(dòng)機(jī)合同的類.(例如: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 contractif ( 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模式指一些提供能夠被一個(gè)或者一組子類簡單繼承功能的類,意在重用其功能。
子類劃分
子類劃分是一個(gè)參考了為一個(gè)新對象繼承來自一個(gè)基類或者超類對象的屬性的術(shù)語.在傳統(tǒng)的面向?qū)ο缶幊讨?類B能夠從另外一個(gè)類A處擴(kuò)展。這里我們將A看做是超類,而將B看做是A的子類。如此,所有B的實(shí)體都從A處繼承了其A的方法,然而B仍然能夠定義它自己的方法,包括那些重載的原本在A中的定義的方法。
B是否應(yīng)該調(diào)用已經(jīng)被重載的A中的方法,我們將這個(gè)引述為方法鏈.B是否應(yīng)該調(diào)用A(超類)的構(gòu)造器,我們將這稱為構(gòu)造器鏈。
為了演示子類劃分,首先我們需要一個(gè)能夠創(chuàng)建自身新實(shí)體的基對象。
let Person = function( firstName , lastName ){this.firstName = firstName;this.lastName = lastName;this.gender = "male";};
接下來,我們將制定一個(gè)新的類(對象),它是一個(gè)現(xiàn)有的Person對象的子類.讓我們想象我們想要加入一個(gè)不同屬性用來分辨一個(gè)Person和一個(gè)繼承了Person"超類"屬性的Superhero.由于超級英雄分享了一般人類許多共有的特征(例如:name,gender),因此這應(yīng)該很有希望充分展示出子類劃分是如何工作的。
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構(gòu)造器創(chuàng)建了一個(gè)自Peroson下降的對象。這種類型的對象擁有鏈中位于它之上的對象的屬性,而且如果我們在Person對象中設(shè)置了默認(rèn)的值,Superhero能夠使用特定于它的對象的值覆蓋任何繼承的值。
Mixin(織入目標(biāo)類)
在Javascript中,我們會(huì)將從Mixin繼承看作是通過擴(kuò)展收集功能的一種途徑.我們定義的每一個(gè)新的對象都有一個(gè)原型,從其中它可以繼承更多的屬性.原型可以從其他對象繼承而來,但是更重要的是,能夠?yàn)槿我鈹?shù)量的對象定義屬性.我們可以利用這一事實(shí)來促進(jìn)功能重用。
Mix允許對象以最小量的復(fù)雜性從它們那里借用(或者說繼承)功能.作為一種利用Javascript對象原型工作得很好的模式,它為我們提供了從不止一個(gè)Mix處分享功能的相當(dāng)靈活,但比多繼承有效得多得多的方式。
它們可以被看做是其屬性和方法可以很容易的在其它大量對象原型共享的對象.想象一下我們定義了一個(gè)在一個(gè)標(biāo)準(zhǔn)對象字面量中含有實(shí)用功能的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!" );}};
然后我們可以方便的擴(kuò)展現(xiàn)有構(gòu)造器功能的原型,使其包含這種使用一個(gè) 如下面的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();
如我們所見,這允許我們將通用的行為輕易的"混"入相當(dāng)普通對象構(gòu)造器中。
在接下來的示例中,我們有兩個(gè)構(gòu)造器:一個(gè)Car和一個(gè)Mixin.我們將要做的是靜Car參數(shù)化(另外一種說法是擴(kuò)展),以便它能夠繼承Mixin中的特定方法,名叫driveForwar()和driveBackward().這一次我們不會(huì)使用Underscore.js。
取而代之,這個(gè)示例將演示如何將一個(gè)構(gòu)造器參數(shù)化,以便在無需重復(fù)每一個(gè)構(gòu)造器函數(shù)過程的前提下包含其功能。
let Car = function ( settings ) {this.model = settings.model || "no model provided";this.color = settings.color || "no colour provided";};// Mixinlet 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();
優(yōu)點(diǎn) & 缺點(diǎn)
Mixin支持在一個(gè)系統(tǒng)中降解功能的重復(fù)性,增加功能的重用性.在一些應(yīng)用程序也許需要在所有的對象實(shí)體共享行為的地方,我們能夠通過在一個(gè)Mixin中維護(hù)這個(gè)共享的功能,來很容易的避免任何重復(fù),而因此專注于只實(shí)現(xiàn)我們系統(tǒng)中真正彼此不同的功能。
也就是說,對Mixin的副作用是值得商榷的.一些開發(fā)者感覺將功能注入到對象的原型中是一個(gè)壞點(diǎn)子,因?yàn)樗鼤?huì)同時(shí)導(dǎo)致原型污染和一定程度上的對我們原有功能的不確定性.在大型的系統(tǒng)中,很可能是有這種情況的。
但是,強(qiáng)大的文檔對最大限度的減少對待功能中的混入源的迷惑是有幫助的,而且對于每一種模式而言,如果在實(shí)現(xiàn)過程中小心行事,我們應(yīng)該是沒多大問題的。
11、裝飾器模式
裝飾器是旨在提升重用性能的一種結(jié)構(gòu)性設(shè)計(jì)模式。同Mixin類似,它可以被看作是應(yīng)用子類劃分的另外一種有價(jià)值的可選方案。
典型的裝飾器提供了向一個(gè)系統(tǒng)中現(xiàn)有的類動(dòng)態(tài)添加行為的能力。其創(chuàng)意是裝飾本身并不關(guān)心類的基礎(chǔ)功能,而只是將它自身拷貝到超類之中。
裝飾器模式并不去深入依賴于對象是如何創(chuàng)建的,而是專注于擴(kuò)展它們的功能這一問題上。不同于只依賴于原型繼承,我們在一個(gè)簡單的基礎(chǔ)對象上面逐步添加能夠提供附加功能的裝飾對象。它的想法是,不同于子類劃分,我們向一個(gè)基礎(chǔ)對象添加(裝飾)屬性或者方法,因此它會(huì)是更加輕巧的。
向Javascript中的對象添加新的屬性是一個(gè)非常直接了當(dāng)?shù)倪^程,因此將這一特定牢記于心,一個(gè)非常簡單的裝飾器可以實(shí)現(xiàn)如下:
示例1:帶有新功能的裝飾構(gòu)造器
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-000let 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: bluelet secondInstance = new vehicle( "car" );console.log( secondInstance );// vehicle: car, model:default, license: 00000-000
示例2:帶有多個(gè)裝飾器的裝飾對象
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() );// 1522console.log( mb.screenSize() );// 11.6
在上面的示例中,我們的裝飾器重載了超類對象MacBook()的 object.cost()函數(shù),使其返回的Macbook的當(dāng)前價(jià)格加上了被定制后升級的價(jià)格。
這被看做是對原來的Macbook對象構(gòu)造器方法的裝飾,它并沒有將其重寫(例如,screenSize()),我們所定義的Macbook的其它屬性也保持不變,完好無缺。
優(yōu)點(diǎn) & 缺點(diǎn)
因?yàn)樗梢员煌该鞯氖褂茫⑶乙蚕喈?dāng)?shù)撵`活,因此開發(fā)者都挺樂意去使用這個(gè)模式——如我們所見,對象可以用新的行為封裝或者“裝飾”起來,而后繼續(xù)使用,并不用去擔(dān)心基礎(chǔ)的對象被改變。在一個(gè)更加廣泛的范圍內(nèi),這一模式也避免了我們?nèi)ヒ蕾嚧罅孔宇悂韺?shí)現(xiàn)同樣的效果。
然而在實(shí)現(xiàn)這個(gè)模式時(shí),也存在我們應(yīng)該意識到的缺點(diǎn)。如果窮于管理,它也會(huì)由于引入了許多微小但是相似的對象到我們的命名空間中,從而顯著的使得我們的應(yīng)用程序架構(gòu)變得復(fù)雜起來。這里所擔(dān)憂的是,除了漸漸變得難于管理,其他不能熟練使用這個(gè)模式的開發(fā)者也可能會(huì)有一段要掌握它被使用的理由的艱難時(shí)期。
足夠的注釋或者對模式的研究,對此應(yīng)該有助益,而只要我們對在我們的應(yīng)程序中的多大范圍內(nèi)使用這一模式有所掌控的話,我們就能讓兩方面都得到改善。
12、亨元模式
享元模式是一個(gè)優(yōu)化重復(fù)、緩慢和低效數(shù)據(jù)共享代碼的經(jīng)典結(jié)構(gòu)化解決方案。它的目標(biāo)是以相關(guān)對象盡可能多的共享數(shù)據(jù),來減少應(yīng)用程序中內(nèi)存的使用(例如:應(yīng)用程序的配置、狀態(tài)等)。
此模式最先由Paul Calder 和 Mark Linton在1990提出,并用拳擊等級中少于112磅體重的等級名稱來命名。享元(“Flyweight”英語中的輕量級)的名稱本身是從以幫以助我們完成減少重量(內(nèi)存標(biāo)記)為目標(biāo)的重量等級推導(dǎo)出的。
實(shí)際應(yīng)用中,輕量級的數(shù)據(jù)共享采集被多個(gè)對象使用的相似對象或數(shù)據(jù)結(jié)構(gòu),并將這些數(shù)據(jù)放置于單個(gè)的擴(kuò)展對象中。我們可以把它傳遞給依靠這些數(shù)據(jù)的對象,而不是在他們每個(gè)上面都存儲(chǔ)一次。
使用享元
有兩種方法來使用享元。第一種是數(shù)據(jù)層,基于存儲(chǔ)在內(nèi)存中的大量相同對象的數(shù)據(jù)共享的概念。第二種是DOM層,享元模式被作為事件管理中心,以避免將事件處理程序關(guān)聯(lián)到我們需要相同行為父容器的所有子節(jié)點(diǎn)上。享元模式通常被更多的用于數(shù)據(jù)層,我們先來看看它。
享元和數(shù)據(jù)共享
對于這個(gè)應(yīng)用程序而言,圍繞經(jīng)典的享元模式有更多需要我們意識到的概念。享元模式中有一個(gè)兩種狀態(tài)的概念——內(nèi)在和外在。內(nèi)在信息可能會(huì)被我們的對象中的內(nèi)部方法所需要,它們絕對不可以作為功能被帶出。外在信息則可以被移除或者放在外部存儲(chǔ)。
帶有相同內(nèi)在數(shù)據(jù)的對象可以被一個(gè)單獨(dú)的共享對象所代替,它通過一個(gè)工廠方法被創(chuàng)建出來。這允許我們?nèi)ワ@著降低隱式數(shù)據(jù)的存儲(chǔ)數(shù)量。
個(gè)中的好處是我們能夠留心于已經(jīng)被初始化的對象,讓只有不同于我們已經(jīng)擁有的對象的內(nèi)在狀態(tài)時(shí),新的拷貝才會(huì)被創(chuàng)建。
我們使用一個(gè)管理器來處理外在狀態(tài)。如何實(shí)現(xiàn)可以有所不同,但針對此的一種方法就是讓管理器對象包含一個(gè)存儲(chǔ)外在狀態(tài)以及它們所屬的享元對象的中心數(shù)據(jù)庫。
經(jīng)典的享元實(shí)現(xiàn)
近幾年享元模式已經(jīng)在Javascript中得到了深入的應(yīng)用,我們會(huì)用到的許多實(shí)現(xiàn)方式其靈感來自于Java和C++的世界。
我們來看下來自維基百科的針對享元模式的 Java 示例的 Javascript 實(shí)現(xiàn)。
在這個(gè)實(shí)現(xiàn)中我們將要使用如下所列的三種類型的享元組件:
享元對應(yīng)的是一個(gè)接口,通過此接口能夠接受和控制外在狀態(tài)。
構(gòu)造享元來實(shí)際的實(shí)際的實(shí)現(xiàn)接口,并存儲(chǔ)內(nèi)在狀態(tài)。構(gòu)造享元須是能夠被共享的,并且具有操作外在狀態(tài)的能力。
享元工廠負(fù)責(zé)管理享元對象,并且也創(chuàng)建它們。它確保了我們的享元對象是共享的,并且可以對其作為一組對象進(jìn)行管理,這一組對象可以在我們需要的時(shí)候查詢其中的單個(gè)實(shí)體。如果一個(gè)對象已經(jīng)在一個(gè)組里面創(chuàng)建好了,那它就會(huì)返回該對象,否則它會(huì)在對象池中新創(chuàng)建一個(gè),并且返回之。
這些對應(yīng)于我們實(shí)現(xiàn)中的如下定義:
CoffeeOrder:享元
CoffeeFlavor:構(gòu)造享元
CoffeeOrderContext:輔助器
CoffeeFlavorFactory:享元工廠
testFlyweight:對我們享元的使用
鴨式?jīng)_減的 “implements”
鴨式?jīng)_減允許我們擴(kuò)展一種語言或者解決方法的能力,而不需要變更運(yùn)行時(shí)的源。由于接下的方案需要使用一個(gè)Java關(guān)鍵字“implements”來實(shí)現(xiàn)接口,而在Javascript本地看不到這種方案,那就讓我們首先來對它進(jìn)行鴨式?jīng)_減。
Function.prototype.implementsFor 在一個(gè)對象構(gòu)造器上面起作用,并且將接受一個(gè)父類(函數(shù)—)或者對象,而從繼承于普通的繼承(對于函數(shù)而言)或者虛擬繼承(對于對象而言)都可以。
// Simulate pure virtual inheritance/"implement" keyword for JSFunction.prototype.implementsFor = function( parentClassOrObject ){if ( parentClassOrObject.constructor === Function ) {// Normal Inheritancethis.prototype = new parentClassOrObject();this.prototype.constructor = this;this.prototype.parent = parentClassOrObject.prototype;} else {// Pure Virtual Inheritancethis.prototype = parentClassOrObject; this.prototype.constructor = this; this.prototype.parent = parentClassOrObject;}return this;};
我們可以通過讓一個(gè)函數(shù)明確的繼承自一個(gè)接口來彌補(bǔ)implements關(guān)鍵字的缺失。下面,為了使我們得以去分配支持一個(gè)對象的這些實(shí)現(xiàn)的功能,CoffeeFlavor實(shí)現(xiàn)了CoffeeOrder接口,并且必須包含其接口的方法。
let CoffeeOrder = {// InterfacesserveCoffee: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());}
轉(zhuǎn)換代碼為使用享元模式
接下來,讓我們通過實(shí)現(xiàn)一個(gè)管理一個(gè)圖書館中所有書籍的系統(tǒng)來繼續(xù)觀察享元。分析得知每一本書的重要元數(shù)據(jù)如下:
ID
標(biāo)題
作者
類型
總頁數(shù)
出版商ID
ISBN
我們也將需要下面一些屬性,來跟蹤哪一個(gè)成員是被借出的一本特定的書,借出它們的日期,還有預(yù)計(jì)的歸還日期。
借出日期
借出的成員
規(guī)定歸還時(shí)間
可用性
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 );}};
這對于最初小規(guī)模的藏書可能工作得還好,然而當(dāng)圖書館擴(kuò)充至每一本書的多個(gè)版本和可用的備份,這樣一個(gè)大型的庫存,我們會(huì)發(fā)現(xiàn)管理系統(tǒng)的運(yùn)行隨著時(shí)間的推移會(huì)越來越慢。使用成千上萬的書籍對象可能會(huì)壓倒內(nèi)存,而我們可以通過享元模式的提升來優(yōu)化我們的系統(tǒng)。
現(xiàn)在我們可以像下面這樣將我們的數(shù)據(jù)分離成為內(nèi)在和外在的狀態(tài):同書籍對象(標(biāo)題,版權(quán)歸屬)相關(guān)的數(shù)據(jù)是內(nèi)在的,而借出數(shù)據(jù)(借出成員,規(guī)定歸還日期)則被看做是外在的。這實(shí)際上意味著對于每一種書籍屬性的組合僅需要一個(gè)書籍對象。這仍然具有相當(dāng)大的數(shù)量,但相比之前已經(jīng)得到大大的縮減了。
下面的書籍元數(shù)據(jù)組合的單一實(shí)體將在所有帶有一個(gè)特定標(biāo)題的書籍拷貝中共享。
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;};
如我們所見,外在狀態(tài)已經(jīng)被移除了。從圖書館借出所要做的一切都被轉(zhuǎn)移到一個(gè)管理器中,由于對象數(shù)據(jù)現(xiàn)在是分段的,工廠可以被用來做實(shí)例化。
一個(gè)基本工廠
現(xiàn)在讓我們定義一個(gè)非常基本的工廠。我們用它做的工作是,執(zhí)行一個(gè)檢查來看看一本給定標(biāo)題的書是不是之前已經(jīng)在系統(tǒng)內(nèi)創(chuàng)建過了;如果創(chuàng)建過了,我們就返回它 - 如果沒有,一本新書就會(huì)被創(chuàng)建并保存,使得以后可以訪問它。
這確保了為每一條本質(zhì)上唯一的數(shù)據(jù),我們只創(chuàng)建了一份單一的拷貝:
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;}}};});
管理外在狀態(tài)
下一步,我們需要將那些從Book對象中移除的狀態(tài)存儲(chǔ)到某一個(gè)地方——幸運(yùn)的是一個(gè)管理器(我們會(huì)將其定義成一個(gè)單例)可以被用來封裝它們。書籍對象和借出這些書籍的圖書館成員的組合將被稱作書籍借出記錄。
這些我們的管理器都將會(huì)存儲(chǔ),并且也包含我們在對Book類進(jìn)行享元優(yōu)化期間剝離的同借出相關(guān)的邏輯。
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 );}};});
這些改變的結(jié)果是所有從Book類中擷取的數(shù)據(jù)現(xiàn)在被存儲(chǔ)到了BookManager單例(BookDatabase)的一個(gè)屬性之中——與我們以前使用大量對象相比可以被認(rèn)為是更加高效的東西。同書籍借出相關(guān)的方法也被設(shè)置在這里,因?yàn)樗鼈兲幚淼臄?shù)據(jù)是外在的而不內(nèi)在的。
這個(gè)過程確實(shí)給我們最終的解決方法增加了一點(diǎn)點(diǎn)復(fù)雜性,然而同已經(jīng)明智解決的數(shù)據(jù)性能問題相比,這只是一個(gè)小擔(dān)憂,如果我們有同一本書的30份拷貝,現(xiàn)在我們只需要存儲(chǔ)它一次就夠了。
每一個(gè)函數(shù)也會(huì)占用內(nèi)存。使用享元模式這些函數(shù)只在一個(gè)地方存在(就是在管理器上),并且不是在每一個(gè)對象上面,這節(jié)約了內(nèi)存上的使用。

