【總結】淺談JavaScript中的接口

什麼是接口

接口是面向對象JavaScript程序員的工具箱中最有用的工具之一。在設計模式中提出的可重用的面向對象設計的原則之一就是「針對接口編程而不是實現編程」,即咱們所說的面向接口編程,這個概念的重要性可見一斑。但問題在於,在JavaScript的世界中,沒有內置的建立或實現接口的方法,也沒有能夠判斷一個對象是否實現了與另外一個對象相同的一套方法,這使得對象之間很難互換使用,好在JavaScript擁有出色的靈活性,這使得模擬傳統面向對象的接口,添加這些特性並不是難事。接口提供了一種用以說明一個對象應該具備哪些方法的手段,儘管它能夠代表這些方法的含義,可是卻不包含具體實現。有了這個工具,就能按對象提供的特性對它們進行分組。例如,假如A和B以及接口I,即使A對象和B對象有極大的差別,只要他們都實現了I接口,那麼在A.I(B)方法中就能夠互換使用A和B,如B.I(A)。還可使用接口開發不一樣的類的共同性。若是把本來要求以一個特定的類爲參數的函數改成要求以一個特定的接口爲參數的函數,那麼全部實現了該接口的對象均可以做爲參數傳遞給它,這樣一來,彼此不相關的對象也能夠被相同地對待。html

接口的利與弊

既定的接口具備自我描述性,並可以促進代碼的重用性,接口能夠提供一種信息,告訴外部一個類須要實現哪些方法。還有助於穩定不一樣類之間的通訊方式,減小了繼承兩個對象的過程當中出現的問題。這對於調試也是有幫助的,在JavaScript這種弱類型語言中,類型不匹配很難追蹤,使用接口時,若是出現了問題,會有更明確的錯誤提示信息。固然接口並不是徹底沒有缺點,若是大量使用接口會必定程度上弱化其做爲弱類型語言的靈活性,另外一方面,JavaScript並無對接口的內置的支持,只是對傳統的面向對象的接口進行模擬,這會使自己較爲靈活的JavaScript變得更加難以駕馭。此外,任何實現接口的方式都會對性能形成影響,某種程度上歸咎於額外的方法調用開銷。接口使用的最大的問題在於,JavaScript不像是其餘的強類型語言,若是不遵照接口的約定,就會編譯失敗,其靈活性能夠有效地避開上述問題,若是是在協同開發的環境下,其接口頗有可能被破壞而不會產生任何錯誤,也就是不可控性。程序員

在面向對象的語言中,使用接口的方式大致類似。接口中包含的信息說明了類須要實現的方法以及這些方法的簽名。類的定義必須明確地聲明它們實現了這些接口,不然是不會編譯經過的。顯然在JavaScript中咱們不能如法炮製,由於不存在interface和implement關鍵字,也不會在運行時對接口是否遵循約定進行檢查,可是咱們能夠經過輔助方法和顯式地檢查模仿出其大部分特性。編程

在JavaScript中模仿接口

在JavaScript中模仿接口主要有三種方式:經過註釋、屬性檢查和鴨式辯型法,以上三種方式有效結合,就會產生相似接口的效果。
註釋是一種比較直觀地把與接口相關的關鍵字(如interfaceimplement等)與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宣稱本身實現了CompositeFormItem這兩個接口,其作法是把這兩個接口的名稱加入一個implementsinterfaces的數組。顯式地聲明本身支持什麼接口。任何一個要求其參數屬性爲特定類型的函數均可以對這個屬性進行檢查,並在所須要的接口未在聲明之中時拋出錯誤。這種方式相對於上一種方式,多了一個強制性的類型檢查。可是這種方法的缺點在於它並未保證類真正地實現了自稱實現的接口,只是知道它聲明本身實現了這些接口。其實類是否聲明本身支持哪些接口並不重要,只要它具備這些接口中的方法就行。鴨式辯型(像鴨子同樣走路而且嘎嘎叫的就是鴨子)正是基於這樣的認識,它把對象實現的方法集做爲判斷它是否是某個類的實例的惟一標準。這種技術在檢查一個類是否實現了某個接口時也能夠大顯身手。這種方法的背後觀點很簡單:若是對象具備與接口定義的方法同名的全部方法,那麼就能夠認爲它實現了這個接口。可使用一個輔助函數來確保對象具備全部必需的方法,代碼以下:app

 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方法的時候,就能夠認定有getDatagetResult兩個方法。可是,構造函數中,只對參數的類型進行了檢查,實際上這並不能保證所須要的方法都獲得了實現。TestResult類會被修改,導致其失去這兩個方法,可是構造器中的檢查依舊會經過,只是renderResult方法再也不有效。
此外,構造器中的這個檢查施加了一些沒必要要的限制。它不容許使用其餘的類的實例做爲參數,不然會直接拋錯,可是問題來了,若是有另外一個類也包含並實現了getDatagetResult方法,它原本能夠被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

 聲明:本博客原創文字只表明本人工做中在某一時間內總結的觀點或結論,與本人所在單位沒有直接利益關係。非商業,未受權貼子請以現狀保留,轉載時必須保留此段聲明,且在文章頁面明顯位置給出原文鏈接。

相關文章
相關標籤/搜索