什么是接口
接口是面向對象JavaScript程序員的工具箱中最有用的工具之一。在設計模式中提出的可重用的面向對象設計的原則之一就是“針對接口編程而不是實現編程”,即我們所說的面向接口編程,這個概念的重要性可見一斑。但問題在於,在JavaScript的世界中,沒有內置的創建或實現接口的方法,也沒有可以判斷一個對象是否實現了與另一個對象相同的一套方法,這使得對象之間很難互換使用,好在JavaScript擁有出色的靈活性,這使得模擬傳統面向對象的接口,添加這些特性並非難事。接口提供了一種用以說明一個對象應該具有哪些方法的手段,盡管它可以表明這些方法的含義,但是卻不包含具體實現。有了這個工具,就能按對象提供的特性對它們進行分組。例如,假如A和B以及接口I,即便A對象和B對象有極大的差異,只要他們都實現了I接口,那么在A.I(B)
方法中就可以互換使用A和B
,如B.I(A)
。還可以使用接口開發不同的類的共同性。如果把原本要求以一個特定的類為參數的函數改為要求以一個特定的接口為參數的函數,那么所有實現了該接口的對象都可以作為參數傳遞給它,這樣一來,彼此不相關的對象也可以被相同地對待。
接口的利與弊
既定的接口具有自我描述性,並能夠促進代碼的重用性,接口可以提供一種信息,告訴外部一個類需要實現哪些方法。還有助於穩定不同類之間的通信方式,減少了繼承兩個對象的過程中出現的問題。這對於調試也是有幫助的,在JavaScript這種弱類型語言中,類型不匹配很難追蹤,使用接口時,如果出現了問題,會有更明確的錯誤提示信息。當然接口並非完全沒有缺點,如果大量使用接口會一定程度上弱化其作為弱類型語言的靈活性,另一方面,JavaScript並沒有對接口的內置的支持,只是對傳統的面向對象的接口進行模擬,這會使本身較為靈活的JavaScript變得更加難以駕馭。此外,任何實現接口的方式都會對性能造成影響,某種程度上歸咎於額外的方法調用開銷。接口使用的最大的問題在於,JavaScript不像是其他的強類型語言,如果不遵守接口的約定,就會編譯失敗,其靈活性可以有效地避開上述問題,如果是在協同開發的環境下,其接口很有可能被破壞而不會產生任何錯誤,也就是不可控性。
在面向對象的語言中,使用接口的方式大體相似。接口中包含的信息說明了類需要實現的方法以及這些方法的簽名。類的定義必須明確地聲明它們實現了這些接口,否則是不會編譯通過的。顯然在JavaScript中我們不能如法炮制,因為不存在interface和implement關鍵字,也不會在運行時對接口是否遵循約定進行檢查,但是我們可以通過輔助方法和顯式地檢查模仿出其大部分特性。
在JavaScript中模仿接口
在JavaScript中模仿接口主要有三種方式:通過注釋、屬性檢查和鴨式辯型法,以上三種方式有效結合,就會產生類似接口的效果。
注釋是一種比較直觀地把與接口相關的關鍵字(如interface
、implement
等)與JavaScript代碼一同放在注釋中來模擬接口,這是最簡單的方法,但是效果最差。代碼如下:
1 //以注釋的形式模仿描述接口 2 /* 3 interface Composite{ 4 function add(child); 5 function remove(child); 6 function getName(index); 7 } 8 9 interface FormItem{ 10 function save(); 11 } 12 */ 13 14 15 //以注釋的形式模仿使用接口關鍵字 16 var CompositeForm =function(id , method,action) { //implements Composite , FormItem 17 // do something 18 } 19 //模擬實現具體的接口方法 此處實現Composite接口 20 CompositeForm.prototype.Add=function(){ 21 // do something 22 } 23 24 CompositeForm.prototype.remove=function(){ 25 // do something 26 } 27 28 CompositeForm.prototype.getName=function(){ 29 // do something 30 } 31 32 //模擬實現具體的接口方法 此處實現FormItem接口 33 Composite.prototype.save=function(){ 34 // do something 35 }
這種方式其實並不是很好,因為這種模仿還只停留在文檔規范的范疇,開發人員是否會嚴格遵守該約定有待考量,對接口的遵守完全依靠開發人員的自覺性。另外,這種方式並不會去檢查某個函數是否真正地實現了我們約定的“接口”。盡管如此,這種方式也有優點,它易於實現而不需要額外的類或者函數,可以提高代碼的可重用性,因為類實現的接口都有注釋說明。這種方式不會影響到文件占用的空間或執行速度,因為注釋的代碼可以在部署的時候輕松剔除。但是由於不會提供錯誤消息,它對測試和調試沒什么幫助。下面的一種方式會對是否實現接口進行檢查,代碼如下:
1 //以注釋的形式模仿使用接口關鍵字 2 var CompositeForm =function(id , method,action) { //implements Composite , FormItem 3 // do something 4 this.implementsinterfaces=['Composite','FormItem']; //顯式地把接口放在implementsinterfaces中 5 } 6 7 8 //檢查接口是否實現 9 function implements(Object){ 10 for(var i=0 ;i< arguments.length;i++){ 11 var interfaceName=arguments[i]; 12 var interfaceFound=false; 13 for(var j=0;j<Object.implementsinterfaces.length;j++){ 14 if(Object.implementsinterfaces[j]==interfaceName){ 15 interfaceFound=true; 16 break; 17 } 18 } 19 if(!interfaceFound){ 20 return false; 21 }else{ 22 return true; 23 } 24 } 25 } 26 27 28 function AddForm(formInstance){ 29 if(!implements(formInstance,'Composite','FormItem')){ 30 throw new Error('Object does not implements required interface!'); 31 } 32 }
上述代碼是在方式一的基礎上進行完善,在這個例子中,CompositeForm
宣稱自己實現了Composite
和FormItem
這兩個接口,其做法是把這兩個接口的名稱加入一個implementsinterfaces
的數組。顯式地聲明自己支持什么接口。任何一個要求其參數屬性為特定類型的函數都可以對這個屬性進行檢查,並在所需要的接口未在聲明之中時拋出錯誤。這種方式相對於上一種方式,多了一個強制性的類型檢查。但是這種方法的缺點在於它並未保證類真正地實現了自稱實現的接口,只是知道它聲明自己實現了這些接口。其實類是否聲明自己支持哪些接口並不重要,只要它具有這些接口中的方法就行。鴨式辯型(像鴨子一樣走路並且嘎嘎叫的就是鴨子)正是基於這樣的認識,它把對象實現的方法集作為判斷它是不是某個類的實例的唯一標准。這種技術在檢查一個類是否實現了某個接口時也可以大顯身手。這種方法的背后觀點很簡單:如果對象具有與接口定義的方法同名的所有方法,那么就可以認為它實現了這個接口。可以使用一個輔助函數來確保對象具有所有必需的方法,代碼如下:
1 //interface 2 var Composite =new Interface('Composite',['add','remove','getName']); 3 var FormItem=new Interface('FormItem',['save']); 4 5 //class 6 var Composite=function(id,method,action){ 7 8 } 9 10 //Common Method 11 function AddForm(formInstance){ 12 ensureImplements(formInstance,Composite,FormItem); 13 //如果該函數沒有實現指定的接口,這個函數將會報錯 14 }
與另外兩種方式不同,這種方式無需注釋,其余的各個方面都是可以強制實施的。EnsureImplements
函數需要至少兩個參數。第一個參數是想要檢查的對象,其余的參數是被檢查對象的接口。該函數檢查器第一個參數代表的對象是否實現了那些接口所聲明的方法,如果漏掉了任何一個,就會拋錯,其中會包含被遺漏的方法的有效信息。這種方式不具備自我描述性,需要一個輔助類和輔助函數來幫助實現接口檢查,而且它只關心方法名稱,並不檢查參數的名稱、數目或類型。
Interface類
在下面的代碼中,對Interface
類的所有方法的參數都進行了嚴格的控制,如果參數沒有驗證通過,那么就會拋出異常。加入這種檢查的目的就是,如果在執行過程中沒有拋出異常,那么就可以肯定接口得到了正確的聲明和實現。
1 var Interface = function(name ,methods){ 2 if(arguments.length!=2){ 3 throw new Error('2 arguments required!'); 4 } 5 this.name=name; 6 this.methods=[]; 7 for(var i=0;len=methods.length;i<len;i++){ 8 if(typeof(methods[i]!=='String')){ 9 throw new Error('method name must be String!'); 10 } 11 this.methods.push(methods[i]); 12 } 13 } 14 15 16 Interface.ensureImplements=function(object){ 17 if(arguments.length<2){ 18 throw new Error('2 arguments required at least!'); 19 } 20 for(var i=0;len=arguments.length;i<len;i++){ 21 var interface=arguments[i]; 22 if(interface.constructor!==Interface){ 23 throw new Error('instance must be Interface!'); 24 } 25 for(var j=0;methodLength=interface.methods.length;j<methodLength;j++){ 26 var method=interface.methods[j]; 27 if(!object[method]||typeof(object[method])=='function')){ 28 throw new Error('object does not implements method!'); 29 } 30 } 31 } 32 }
其實多數情況下,接口並不是經常被使用的,嚴格的類型檢查並不總是明智的。但是在設計復雜的系統的時候,接口的作用就體現出來了,這看似降低了靈活性,卻同時也降低了耦合性,提高了代碼的重用性。這在大型系統中是比較有優勢的。在下面的例子中,聲明了一個displayRoute
方法,要求其參數具有三個特定的方法,通過Interface
對象和ensureImplements
方法來保證這三個方法的實現,否則將會拋出錯誤。
1 //聲明一個接口,描述該接口包含的方法 2 var DynamicMap=new Interface{'DynamicMap',['centerOnPoint','zoom','draw']}; 3 4 //聲明一個displayRoute方法 5 function displayRoute(mapInstance){ 6 //檢驗該方法的map 7 //檢驗該方法的mapInsstance是否實現了DynamicMap接口,如果未實現則會拋出 8 Interface.ensureImplements(mapInstance,DynamicMap); 9 //如果實現了則正常執行 10 mapInstance.centerOnPoint(12,22); 11 mapInstance.zoom(5); 12 mapInstance.draw(); 13 }
下面的例子會將一些數據以網頁的形式展現出來,這個類的構造器以一個TestResult的實例作為參數。該類會對TestResult
對象所包含的數據進行格式化(Format)后輸出,代碼如下:
1 var ResultFormatter=function(resultObject){ 2 //對resultObject進行檢查,保證是TestResult的實例 3 if(!(resultObject instanceof TestResult)){ 4 throw new Error('arguments error!'); 5 } 6 this.resultObject=resultObject; 7 } 8 9 ResultFormatter.prototype.renderResult=function(){ 10 var dateOfTest=this.resultObject.getData(); 11 var resultArray=this.resultObject.getResults(); 12 var resultContainer=document.createElement('div'); 13 var resultHeader=document.createElement('h3'); 14 resultHeader.innerHTML='Test Result from '+dateOfTest.toUTCString(); 15 resultContainer.appendChild(resultHeader); 16 17 var resultList=document.createElement('ul'); 18 resultContainer.appendChild(resultList); 19 20 for(var i=0;len=resultArray.length;i<len;i++){ 21 var listItem=document.createElement('li'); 22 listItem.innerHTML=resultArray[i]; 23 resultList.appendChild('listItem'); 24 } 25 return resultContainer; 26 } 27 28
該類的構造器會對參數進行檢查,以確保其的確為TestResult
的類的實例。如果參數達不到要求,構造器將會拋出一個錯誤。有了這樣的保證,在編寫renderResult
方法的時候,就可以認定有getData
和getResult
兩個方法。但是,構造函數中,只對參數的類型進行了檢查,實際上這並不能保證所需要的方法都得到了實現。TestResult
類會被修改,致使其失去這兩個方法,但是構造器中的檢查依舊會通過,只是renderResult
方法不再有效。
此外,構造器中的這個檢查施加了一些不必要的限制。它不允許使用其他的類的實例作為參數,否則會直接拋錯,但是問題來了,如果有另一個類也包含並實現了getData
和getResult
方法,它本來可以被ResultFormatter
使用,卻因為這個限制而無用武之地。
解決問題的辦法就是刪除構造器中的校驗,並使用接口代替。我們采用這個方案對代碼進行優化:
1 //接口的聲明 2 var resultSet =new Interface('ResultSet',['getData','getResult']); 3 4 //修改后的方案 5 var ResultFormatter =function(resultObject){ 6 Interface.ensureImplements(resultObject,resultSet); 7 this.resultObject=resultObject; 8 }
上述代碼中,renderResult
方法保持不變,而構造器卻采用的ensureImplements
方法,而不是typeof
運算符。現在的這個構造器可以接受任何符合接口的類的實例了。
依賴於接口的設計模式
<1>工廠模式:對象工廠所創建的具體對象會因具體情況而不同。使用接口可以確保所創建的這些對象可以互換使用,也就是說對象工廠可以保證其生產出來的對象都實現了必需的方法;
<2>組合模式:如果不使用接口就不可能使用這個模式,其中心思想是可以將對象群體與其組成對象同等對待。這是通過接口來做到的。如果不進行鴨式辯型或類型檢查,那么組合模式就會失去大部分意義;
<3>裝飾者模式:裝飾者通過透明地為另一個對象提供包裝而發揮作用。這是通過實現與另外那個對象完全一致的接口實現的。對於外界而言,一個裝飾者和它所包裝的對象看不出有什么區別,所以使用Interface來確保所創建的裝飾者實現了必需的方法;
<4>命令模式:代碼中所有的命令對象都有實現同一批方法(如run、ecxute、do等)通過使用接口,未執行這些命令對象而創建的類可以不必知道這些對象具體是什么,只要知道他們都正確地實現了接口即可。借此可以創建出模塊化程度很高的、耦合度很低的API。
作者:悠揚的牧笛
博客地址:http://www.cnblogs.com/xhb-bky-blog/p/5887242.html
聲明:本博客原創文字只代表本人工作中在某一時間內總結的觀點或結論,與本人所在單位沒有直接利益關系。非商業,未授權貼子請以現狀保留,轉載時必須保留此段聲明,且在文章頁面明顯位置給出原文連接。