1.簡介
在Ajax技術出現之前,傳統的Web應用中JavaScript代碼所占的比例較少,并且大多數情況下都是采用面向過程的程式設計方式,真正将JavaScript作為一門進階語言進行正式程式設計的開發人員也很少。而目前富用戶端的Web應用開發已經成為大勢所趨,JavaScript在Web應用中所占的比例會越來越大。如何有效地實作代碼的複用以及靈活應對需求的變化已經正式列入JavaScript程式員必須探讨的議事日程。
GoF的《DesignPatterns:Elements of Reusable Object-OrientedSoftware》一書成為當代程式員研究設計模式的典範與開山之作。然而,目前針對設計模式的探讨主要停留在以Java與C#等強類型進階語言方面,那麼,對于像JavaScript這樣的弱類型語言是否也适用設計模式呢?答案是肯定的。在本文中,我将通過幾個簡短的示例來向你展示如何把設計模式應用于像JavaScript這樣的弱類型語言的程式設計之中。
2.Singleton模式(亦稱“單例模式”)
(1)概念
Singleton模式作為一種非常基本和重要的建立型模式,其職責是保證一個類有且隻有一個執行個體,并提供一個通路它的全局通路點。
在程式設計過程中,有很多情況下需要確定一個類隻能有一個執行個體。傳統的程式設計語言中為了使一個類隻有一個執行個體,最容易的方法是在類中嵌入靜态變量,并在第一個執行個體中設定該變量,而且每次進入構造函數都要做檢查,不管類有多少個執行個體,靜态變量隻能有一個執行個體。為了防止類被多次初始化,需要把構造函數聲明為私有類型,這樣便隻能在靜态方法裡建立一個執行個體。
在JavaScript中,雖然我們仍然可以指定靜态方法來構造對象,但由于我們不能利用構造函數的“私有”來禁止多個執行個體的生成,是以要完全實作Singleton并沒有想象中那麼簡單。
(2)舉例
假設有一個作為工具類使用的對象,它不包含任何業務邏輯,也不包含任何需要改變的資料。使用這樣的對象時不需要每次都進行執行個體化,隻需要存在一個全局的對象供調用即可。
根據剛才的分析,我們就可以在JavaScript中通過如清單1所示的方式實作Singleton模式。模式可以保證一個類僅有一個執行個體,并且提供一個通路它的全局通路點。
清單1:在JavaScript中建立Singleton模式類
function Singleton(){ this.property1=”hello”; this.methodl=function(x,y){ return x+y; } //定義一個靜态屬性 Singleton._instance_=null; Singleton.getInstance=function(){ if(this._instance_==null){ //如果Singleton執行個體不存在,則進行執行個體化 this._instance_=new Singleton(); } //返到Singleton的對象執行個體 return this._instance_; }; |
上面的getInstance方法首先判斷靜态屬性Singleton._instance_是否為null;如果為null,則建立一個Singleton類的對象,儲存在Singleton._instance_,最後傳回Singleton._instance_屬性。
至于Singleton類的使用方法則很簡單:
var instA =Singleton.getInstance(); //傳回Singleton類的唯一執行個體 |
但遺憾的是,以上的代碼不能禁止使用者直接執行個體化Singleton類。例如,通過以下語句執行個體化Singleton類仍然是成立的:
var instA =new Singleton (); |
而随後你調用如下語句也是成立的:
alert(typeof(instA)); alert(instA.property1); |
是以,我們需要進一步修改Singleton類的構造函數,使得Singleton類隻能在Singleton.getInstance方法中進行執行個體化。
function Singleton(){ if(Singleton.caller!=Singleton.getInstance){ throw new Error(”Can not new Singleton instance!”); } ………… } |
這樣當使用者試圖自己建立多個對象的時候,通過人工抛出異常來阻止。不過這麼做你感覺是不是還是有一點别扭?建立單例就建立單例吧,幹嘛非要抛出一個異常呢?但無論如何,使用上述方案已經能夠實作了僅能建立類的單例的目的。
下面,我們來讨論第三種方法(也是最優秀的方法)。這種方法巧妙利用了JavaScript對于“匿名”函數的支援特征來間接實作禁止對Singleton類構造函數的通路,進而較好地模拟了私有構造函數的特性,最終比較完美地解決了用JavaScript實作Singleton模式的問題。
清單2:借助于“匿名”函數的支援在JavaScript中建立Singleton模式類
<script type="text/javascript"> (function(){ //SingletonFactory Interface SingletonFactory = { getInstance : getInstance } //private classes function SingletonObject() { SingletonObject.prototype.methodA = function() { alert('methodA'); } SingletonObject.prototype.methodB = function() { alert('methodB'); } SingletonObject.instance = this; } //SingletonFactory implementions function getInstance() { if(SingletonObject.instance == null) return new SingletonObject(); else return SingletonObject.instance; } })(); var instA = null; try { alert("試圖通過new SingletonObject()構造執行個體!"); instA = new SingletonObject(); } catch(e){ alert("SingletonObject構造函數不能從外部通路,系統抛出了異常!"); } instA = SingletonFactory.getInstance(); //通過Factory上定義的靜态方法獲得 var instB = SingletonFactory.getInstance(); instA.methodA(); instB.methodA(); alert(instA == instB); //成功 </script> |
上面的第三種建立方式可謂挖空心思,窮JavaScript之能事,但終于擷取了令人滿意的效果。
3.Factory Method模式
(1)概念
根據GoF的定義,FactoryMethod模式的目的是為了定義一個建立對象的接口,由子類來決定執行個體化哪一個類。更準确地說應該是,FactoryMethod模式是将建立對象執行個體的責任轉移到工廠類中,并利用抽象的原理,将執行個體化行為延遲到具體工廠類中。
(2)舉例
在某些情況下,我們的确無法确定将要建立的對象是哪個類的執行個體,這樣的一個典型示例就是在Ajax應用程式中需要建立XMLHttpRequest(XHR)對象時,因為大家都知道在不同的浏覽器中XHR對象的實作類是不同的。
通過引入Factory Method模式,我們即可以輕松使用JavaScript實作建立通用的XHR對象,相應的代碼實作如下所示。
function XMLHttpFactory(){} XMLHttpFactorv.CreateXMLHttp=function() { if(typeof XMLHttpRequest!=”undefined”){ //支援XMLHttpRequest對象的浏覽器:例如Firefox等 return new XMLHttpRequest(); }else if(typeof window.ActiveXObject!=”undefined”){ //支援ActiveX對象的浏覽器,即IE return new ActiveXObject(“MSXML2.XMLHttp”); } } |
然後,通過以下代碼便可以相當容易地判斷出浏覽器的類型并進而建立XHR對象。
var xmlhttp=XMLHttpFactory.createXMLHttp(); |
此後,基于以上建立的XHR對象就可以進行Ajax風格的伺服器端調用了。
4.Decorator模式
(1)概念
Decorator模式是結構型模式的一種,它充分利用了繼承與聚合的優勢,其主要目的是為了給一個對象動态地添加以新的職責,而此職責并不修改原有的行為,而是在原有行為基礎上添加新的功能,就好比裝飾勞工為一座新居的牆上塗抹上色彩缤紛的顔料一般。
【備注】面向方面程式設計(AOP),是一種改進已經存在的模式和發現新模式的方法。面向方面的程式設計能夠獨立于任何繼承層次結構而應用改變類或對象行為的方面。然後,在運作時或編譯時應用這些方面。AOP程式設計引入了一組新概念:接合點(Joinpoint)—代碼中定義明确的可識别的點。切點(Pointcut)—通過配置或編碼指定接合點的一種方法。通知(Advice)—表示需要執行交叉切割動作的一種方法。混入(Mixin)—通過将一個類的執行個體混入目标類的執行個體引入新行為。對于好的程式設計來說,設計模式已經變成了常用的方式。AOP可以給我們實際上,橫切關注點代碼的注入,是以說它也就是一種新式類型的軟體設計模式。
(2)舉例
有意思的是,我們也可以把Decorator模式應用于JavaScript中實作面向方面程式設計(AOP)。現在,不妨讓我們觀察一下清單3中定義的代碼。
清單3:使用Decorator模式在JavaScript中實作面向方面程式設計(AOP)
function Decorator(object){ object.setup=function(method){ //如果從未執行過Setup方法,則進行初始化 if(!(‘_’+method in object)){ //保留原有的method方法 object[‘_’+method]=object[method]; //初始化前置和後置通知的清單 object[‘beforeAdvice_’+method]=[]; object[‘afterAdvice_’+method] =[]; //改寫method方法 object[method]=function(){ //前置通知清單 var before=this[‘beforeAdvice_’+method]; //後置通知清單 var after=this[‘afterAdvice_’+method]; //傳回值 var ret; //執行前置通知中的函數 for(var i=0; i<before.length; i++) { before[i].call(this,arguments); } //執行原有的方法 ret=this[‘_’ +method](arguments); //調用後置通知中的函數 for (var i=; i<after.1ength; i++){ after[i].call(this, arguments); } //傳回原有方法的傳回值 return ret; } } }; //添加前置通知 object.addBeforeAdvice=function(method, f) { object.setup(method); //最後添加的前置通知将最先被執行 object[‘beforeAdvice_’ +method].unshift(f); }; //添加後置通知 object.addAfterAdvice=function(method, f) { object.Setup(method); //最後添加的後置通知将最後被執行 object[‘afterAdvice_’ +method].push(f); }; } |
Decorator函數的基本思路是将原有的函數替換,在替換後的函數中增加對前置、後置通知的調用。下面通過一個具體的示例來說明Decorator函數的使用方法,首先定義一個類testClass。
var testClass = function() { testClass.prototype.pl=”hello”; testClass.prototype.ml =function() { alert(this.p1); } }; |
如果希望在所有testClass類的執行個體上增加前置或後置通知,那麼需要對testClass.prototype屬性進行處理。
Decorator(testClass.prototype); testClass.prototype.addBeforeAdvice(“m1”, function() { alert(“beforeAdvice”); }); testClass.prototype.addAfterAdvice(“m1”, function() { alert(”afterAdvice”); }); |
此時,建立一個testClass類的對象執行個體,并且執行它的ml方法,程式将依次輸出“beforeAdvice”、“hello”和“afterAdvice”。
var t=new testClass (); t.ml(); //依次輸出“beforeAdvice”、“hello”和“afterAdvice” |
如果僅希望對testClass類的某個具體執行個體添加前置、後置通知,那麼直接處理該執行個體即可。
var t=new testClass(); Decorator(t); t.addBeforeAdvice(“ml” ,function](){ alert(“beforeAdvice”); }); t.addAfterAdvice(“ml”, function(){ alert(“afterAdvice”); }); |
5.Observer模式
(1)概念
使用Observer模式的目的是為了定義對象之間一對多的依賴關系,當一個對象的狀态發生改變時,所有依賴于它的對象都得到通知并且自動更新。在以下情況下,一般可以考慮使用Observer模式。
·當一個抽象模型有兩個方面,其中一個方面依賴于另一方面。将這二者封裝在獨立的對象中以便使它們可以各自獨立地改變和複用。
·當對一個對象的改變需要同時改變其他對象,而不知道具體有多少對象有待改變。
·當一個對象必須通知其他對象,而它又不能假定其他對象是誰。換言之,你不希望這些對象是緊密耦合的。
(2)舉例
下面,我們将應用Observer模式實作簡單的JavaScript事件處理機制。首先需要做一些準備工作,即對Array對象進行擴充,為其增加indexOf和removeAt方法,代碼如下:
//擴充destination對象的屬性,将source對象的所有屬性複制到destination對象 Object.extend=function(destination,source){ for(property in source){ destination[property]=source[property]; } return destination; } //擴充Array對象的屬性 Object.extend(Array.prototype,{ //在數組中查找object對象 indexOf: function (object){ for(var i=0,1ength=this.length; i<length; i++) if(this[i]==object) return i; return -1; }, //将數組中指定位置的元素删除 removeAt: function (index){ if(index<0 || index>=this.length) return null; switch(index){ case 0: return this.shift(); break; case this.length -1: return this.pop(); break; default: var head=this.s1ice(0,index); var tail=this.s1ice(index+1); var ele=this[index]; this=head.concat(tail); return ele; break; } } }); |
接下來,定義Observer類和Subject類。其中,Observer類的代碼如下所示:
//Observer類 function Observer(){} object.extend(Observer.prototype, { //實體Observer類需要覆寫Update方法 Update:function(){ return; } }); |
Subject類的代碼如下:
//Subject類 function Subject(){} Object.extend(Subject.prototype,{ //Observer對象的數組 observers:[], //通知每一個Observer對象執行Update方法 notify:function(context){ for(var i=0; i<this.observers.length; i++) this.observers[i].Update(context); }, //添加一個新的Observer addObserver:function(observer) { if(!observer.Update) throw new Error(“addObserver error!”); this.observers.push(observer); }, //删除已有的Observer removeObserver:function(Observer) { if(!observer.Update) throw new Error(“removeObserver error!”); this.observers.removeAt(this.observers.indexOf(observer)); }, }); |
到此為止,使用Observer模式所需要的類都已經實作。下面是使用這些類實作Observer模式的步驟:
·建立Subject對象執行個體;
·建立若幹個Observer對象執行個體,分别覆寫它們的Update方法;
·将Observer對象執行個體訂閱到Subject對象上;
·通過Subject對象發出通知,此時所有訂閱了該Subject對象的Observer對象均會執行各自的Update方法;
·如果需要撤銷某個Observer對象對Subject對象的訂閱,可以調用removeObserver方法實作。
最後,我們通過一個執行個體來示範Observer模式的應用。首先,建立一個Subject對象并且為該對象定義publishEvent方法。
//建立一個Subject對象 var concreteSubject=new Subject(); //定義publishEvent方法,該方法的作用是通知所有的Observer,通知的内容是data concreteSubject.publishEvent=function(data){ document.write(“published:”+data); this.notify(data); } |
//建立一個Subject對象 var concreteSubject=new Subject(); //定義publishEvent方法,該方法的作用是通知所有的Observer,通知的内容是data concreteSubject.publishEvent=function(data) ( document.write(“published:”+data); this.notify(data); } //建立一個Observer對象,并覆寫其Update方法 var concreteObserverl=new Observer(); concreteObserverl.Update=function(data){ document.write(“concreteObserverl received:”+data); } //将concreteObserverl訂閱到concreteSubject concreteSubject.addObserver(concreteObserverl); //建立第二個Observer對象,并覆寫其Update方法 var concreteObserver2=new Observer(); concreteObserver2.Update=function(data){ document.write(“concreteObserver2 received:”+data); } //将concreteObserver2訂閱到concreteSubject concreteSubject.addObserver(concreteObserver2); |
執行concreteSubject對象的publishEvent方法,發出通知。
concreteSubject.publishEvent(“msg”); |
此時程式将輸出:
published:msg concreteObserverl received:msg concreteObserver2 receired:msg |
這樣的輸出結果說明concreteObserverl和concreteObserver2都接收到來自concreteSubject的通知。接下來調用removeObserver方法撤消一個concreteSubject的訂閱。
concreteSubject.removeObserver(concreteObserver2); |
再次發出通知:
concreteSubject.publishEvent(“msg”); |
這一次隻有concreteObserver1對象接收到通知,程式輸出結果為:
published:msg concreteObserver1 receired:msg |
使用Observer模式可以在JavaScript中實作通用的事件機制。有些熟悉Ajax開發的朋友可能知道Prototype架構和Dojo架構這兩個用戶端架構。其實,這兩個架構的事件機制就是采用了類似此處介紹的方法實作的,有興趣的朋友可進一步研究。
6. Fa?ade模式
(1)概念
Fa?ade模式可以為子系統中的一組接口提供一個一緻的界面,它定義了一個高層接口,這個接口使得相應的子系統更加容易使用。Fa?ade模式在Web開發中的一個典型應用是實作統一的API接口,消除各種浏覽器在具體特性實作中的差異。
(2)舉例
在基于Ajax的Web開發中,當使用XHR對象請求伺服器端的資料時,我們通常需要按照以下的步驟進行程式設計:
·建立XHR對象;
·注冊回調函數;
·用open方法設定請求方式(POST/GET)、URL和模式(同步/異步);
·用send發送請求;
·監視請求的狀态,當達到某一特定狀态時,執行某項特殊功能。
可以看到,使用XHR對象的步驟是比較複雜的,在每個步驟中都需要考慮浏覽器相容性問題。
【補充】熟悉Prototype架構的朋友可能知道此架構提供了一個Ajax.Request對象,使用該對象不必勞開發人員考慮以上這些繁瑣步驟以及步驟之後的細節,通過統一的方式即可調用。有興趣的讀者可以下載下傳Prototype的源碼自行學習Ajax.Request對象的具體實作方法,在此不再贅述。
下面,我們将通過一個簡單的示例來說明Fa?ade模式的應用。該示例的目标是向使用者輸出一行歡迎資訊,提示使用者使用的浏覽器類型。為了簡單起見,示例中僅區分IE和其他浏覽器。相關的代碼如下所示:
//IE浏覽器對象 function IEBrowser() { this.hello=function(){ alert(”IE browser”); } } //非IE浏覽器對象 function NonIEBrowser() ( this.hello =function(){ alert(“NonIE browser”); } } //Facade對象 var Facade={}; Facade.hello=function(){ var browser; //如果為IE浏覽器 if(window.ActiveXObject) browser=new IEBrowser(); //如果為非IE浏覽器 else browser=new NonIEBrowser(); Browser.hello(); ); |
Facade.hello方法根據浏覽器類型,建立不同的browser對象,再調用browser對象的hello方法。這樣對于使用者而言,無需考慮浏覽器的類型,隻需要簡單地調用Facade對象的hello方法即可。
另外,IEBrowser和NonIEBrowser分别是兩個獨立的類,它們的内部實作可能會在未來發生變化,但是這都不會影響到外部對象Facade對它們的調用,即Facade.hello的代碼無需改變。
7. Command模式
(1)概念
Command模式把一個請求或操作封裝到一個對象中。具體地說,Command模式允許系統使用不同的請求把用戶端參數化,對請求排隊或者記錄請求日志,可以提供指令的撤銷和恢複功能。Command模式把發出指令的責任和執行指令的責任分割開,委派給不同的對象。
(2)舉例
下面我們通過一個JavaScript電腦程式的設計思路來說明Command模式的應用。
首先,我們馬上會想到定義如下的Calculator對象,其中包括了定義四則運算的四個方法。
var Calculator={ //加法 add: function(x,y) { return x+y; }, //減法 substract: function(x, y) { return x-y; }, //乘法 multiply: function(x, y) { return x*y; }, //除法 divide: function(x, y) { return x/y; }, }; |
誠然,基于以上的Calculator對象,我們可以直接調用Calculator.add等方法來實作基本的四則運算;但是,在某些情況下我們不希望直接調用Calculator對象内部的方法,因為這樣會增加對象之間的依賴關系。也就是說,如果Calculator對象的内部實作發生了變化,那麼調用Calculator進行計算的相關程式代碼也必須同時進行相應的修改。這樣以來,就與面向對象設計中應當盡可能減少對象之間耦合度的理念相違背。
根據設計模式的相關理論,我們可以嘗試使用Command模式來改進以上設計。在此,使用Command模式的基本思想是:将任何類型的計算都看作是對Calculator對象的一個請求,請求的内容包括運算的類型和參與運算的操作數。于是,通過如下的代碼即可計算出“1+2”的運算結果。
Calculator.calc({ type: “add”, //加法運算 op1:1, //第1個操作數為l op2:2 //第2個操作數為2 }); |
根據上面的使用格式,我們可以得到如下所示的Calculator.calc方法定義:
Calculator.calc=function(command) { return Calculator[command.type](command.opl,command.op2); }; |
這樣以來,我們就可以通過如下Calculator.calc方法來實作上述各類運算:
Calculator.calc({type: “add”,opl:1,op2:1});//加法運算,傳回2 Calculator.calc({type: “Substract”,opl:6,op2:2});//減法運算,傳回4 Calculator.calc({type: “multiply”,opl:5,op2:2)); //乘法運算,傳回10 Calculator.calc({type: “divide”,opl:8,op2:4));//除法運算,傳回2 |
8. 小結
基于設計模式程式設計的思想盡管已經提出好幾年了,但是僅僅在最近幾年才引起第一線開發人員的重視,這從網際網路上衆多的資料不難得知。設計模式是基于面向對象程式設計的,是以掌握起來并不容易,特别是它需要在軟體開發不斷地實踐才能最終靈活運用各種設計模式。
設計模式不依賴于任何語言,是以在JavaScript開發中同樣适用,僅僅是這方面的資料甚少罷了。本文正是基于用戶端常見的JavaScript開發執行個體,重點讨論了Singleton、FactoryMethod、Decorator、Observer、Fa?ade、Command共6種典型設計模式的運用。