淺談JavaScript中的接口實(shí)現(xiàn)
一、什么是接口
接口是面向?qū)ο驤avaScript程序員的工具箱中最有用的工具之一。在設(shè)計(jì)模式中提出的可重用的面向?qū)ο笤O(shè)計(jì)的原則之一就是“針對(duì)接口編程而不是實(shí)現(xiàn)編程”,即我們所說(shuō)的面向接口編程,這個(gè)概念的重要性可見一斑。但問(wèn)題在于,在JavaScript的世界中,沒有內(nèi)置的創(chuàng)建或?qū)崿F(xiàn)接口的方法,也沒有可以判斷一個(gè)對(duì)象是否實(shí)現(xiàn)了與另一個(gè)對(duì)象相同的一套方法,這使得對(duì)象之間很難互換使用,好在JavaScript擁有出色的靈活性,這使得模擬傳統(tǒng)面向?qū)ο蟮慕涌冢砑舆@些特性并非難事。接口提供了一種用以說(shuō)明一個(gè)對(duì)象應(yīng)該具有哪些方法的手段,盡管它可以表明這些方法的含義,但是卻不包含具體實(shí)現(xiàn)。有了這個(gè)工具,就能按對(duì)象提供的特性對(duì)它們進(jìn)行分組。例如,假如A和B以及接口I,即便A對(duì)象和B對(duì)象有極大的差異,只要他們都實(shí)現(xiàn)了I接口,那么在A.I(B)方法中就可以互換使用A和B,如B.I(A)。還可以使用接口開發(fā)不同的類的共同性。如果把原本要求以一個(gè)特定的類為參數(shù)的函數(shù)改為要求以一個(gè)特定的接口為參數(shù)的函數(shù),那么所有實(shí)現(xiàn)了該接口的對(duì)象都可以作為參數(shù)傳遞給它,這樣一來(lái),彼此不相關(guān)的對(duì)象也可以被相同地對(duì)待。
二、接口的利與弊
既定的接口具有自我描述性,并能夠促進(jìn)代碼的重用性,接口可以提供一種信息,告訴外部一個(gè)類需要實(shí)現(xiàn)哪些方法。還有助于穩(wěn)定不同類之間的通信方式,減少了繼承兩個(gè)對(duì)象的過(guò)程中出現(xiàn)的問(wèn)題。這對(duì)于調(diào)試也是有幫助的,在JavaScript這種弱類型語(yǔ)言中,類型不匹配很難追蹤,使用接口時(shí),如果出現(xiàn)了問(wèn)題,會(huì)有更明確的錯(cuò)誤提示信息。當(dāng)然接口并非完全沒有缺點(diǎn),如果大量使用接口會(huì)一定程度上弱化其作為弱類型語(yǔ)言的靈活性,另一方面,JavaScript并沒有對(duì)接口的內(nèi)置的支持,只是對(duì)傳統(tǒng)的面向?qū)ο蟮慕涌谶M(jìn)行模擬,這會(huì)使本身較為靈活的JavaScript變得更加難以駕馭。此外,任何實(shí)現(xiàn)接口的方式都會(huì)對(duì)性能造成影響,某種程度上歸咎于額外的方法調(diào)用開銷。接口使用的***的問(wèn)題在于,JavaScript不像是其他的強(qiáng)類型語(yǔ)言,如果不遵守接口的約定,就會(huì)編譯失敗,其靈活性可以有效地避開上述問(wèn)題,如果是在協(xié)同開發(fā)的環(huán)境下,其接口很有可能被破壞而不會(huì)產(chǎn)生任何錯(cuò)誤,也就是不可控性。
在面向?qū)ο蟮恼Z(yǔ)言中,使用接口的方式大體相似。接口中包含的信息說(shuō)明了類需要實(shí)現(xiàn)的方法以及這些方法的簽名。類的定義必須明確地聲明它們實(shí)現(xiàn)了這些接口,否則是不會(huì)編譯通過(guò)的。顯然在JavaScript中我們不能如法炮制,因?yàn)椴淮嬖趇nterface和implement關(guān)鍵字,也不會(huì)在運(yùn)行時(shí)對(duì)接口是否遵循約定進(jìn)行檢查,但是我們可以通過(guò)輔助方法和顯式地檢查模仿出其大部分特性。
三、在JavaScript中模仿接口
在JavaScript中模仿接口主要有三種方式:通過(guò)注釋、屬性檢查和鴨式辯型法,以上三種方式有效結(jié)合,就會(huì)產(chǎn)生類似接口的效果。
注釋是一種比較直觀地把與接口相關(guān)的關(guān)鍵字(如interface、implement等)與JavaScript代碼一同放在注釋中來(lái)模擬接口,這是最簡(jiǎn)單的方法,但是效果最差。代碼如下:
- //以注釋的形式模仿描述接口
- /*
- interface Composite{
- function add(child);
- function remove(child);
- function getName(index);
- }
- interface FormItem{
- function save();
- }
- */
- //以注釋的形式模仿使用接口關(guān)鍵字
- var CompositeForm =function(id , method,action) { //implements Composite , FormItem
- // do something
- }
- //模擬實(shí)現(xiàn)具體的接口方法 此處實(shí)現(xiàn)Composite接口
- CompositeForm.prototype.Add=function(){
- // do something
- }
- CompositeForm.prototype.remove=function(){
- // do something
- }
- CompositeForm.prototype.getName=function(){
- // do something
- }
- //模擬實(shí)現(xiàn)具體的接口方法 此處實(shí)現(xiàn)FormItem接口
- Composite.prototype.save=function(){
- // do something
- }
這種方式其實(shí)并不是很好,因?yàn)檫@種模仿還只停留在文檔規(guī)范的范疇,開發(fā)人員是否會(huì)嚴(yán)格遵守該約定有待考量,對(duì)接口的遵守完全依靠開發(fā)人員的自覺性。另外,這種方式并不會(huì)去檢查某個(gè)函數(shù)是否真正地實(shí)現(xiàn)了我們約定的“接口”。盡管如此,這種方式也有優(yōu)點(diǎn),它易于實(shí)現(xiàn)而不需要額外的類或者函數(shù),可以提高代碼的可重用性,因?yàn)轭悓?shí)現(xiàn)的接口都有注釋說(shuō)明。這種方式不會(huì)影響到文件占用的空間或執(zhí)行速度,因?yàn)樽⑨尩拇a可以在部署的時(shí)候輕松剔除。但是由于不會(huì)提供錯(cuò)誤消息,它對(duì)測(cè)試和調(diào)試沒什么幫助。下面的一種方式會(huì)對(duì)是否實(shí)現(xiàn)接口進(jìn)行檢查,代碼如下:
- //以注釋的形式模仿使用接口關(guān)鍵字
- var CompositeForm =function(id , method,action) { //implements Composite , FormItem
- // do something
- this.implementsinterfaces=['Composite','FormItem']; //顯式地把接口放在implementsinterfaces中
- }
- //檢查接口是否實(shí)現(xiàn)
- function implements(Object){
- for(var i=0 ;i< arguments.length;i++){
- var interfaceName=arguments[i];
- var interfaceFound=false;
- for(var j=0;j<Object.implementsinterfaces.length;j++){
- if(Object.implementsinterfaces[j]==interfaceName){
- interfaceFound=true;
- break;
- }
- }
- if(!interfaceFound){
- return false;
- }else{
- return true;
- }
- }
- }
- function AddForm(formInstance){
- if(!implements(formInstance,'Composite','FormItem')){
- throw new Error('Object does not implements required interface!');
- }
- }
上述代碼是在方式一的基礎(chǔ)上進(jìn)行完善,在這個(gè)例子中,CompositeForm宣稱自己實(shí)現(xiàn)了Composite和FormItem這兩個(gè)接口,其做法是把這兩個(gè)接口的名稱加入一個(gè)implementsinterfaces的數(shù)組。顯式地聲明自己支持什么接口。任何一個(gè)要求其參數(shù)屬性為特定類型的函數(shù)都可以對(duì)這個(gè)屬性進(jìn)行檢查,并在所需要的接口未在聲明之中時(shí)拋出錯(cuò)誤。這種方式相對(duì)于上一種方式,多了一個(gè)強(qiáng)制性的類型檢查。但是這種方法的缺點(diǎn)在于它并未保證類真正地實(shí)現(xiàn)了自稱實(shí)現(xiàn)的接口,只是知道它聲明自己實(shí)現(xiàn)了這些接口。其實(shí)類是否聲明自己支持哪些接口并不重要,只要它具有這些接口中的方法就行。鴨式辯型(像鴨子一樣走路并且嘎嘎叫的就是鴨子)正是基于這樣的認(rèn)識(shí),它把對(duì)象實(shí)現(xiàn)的方法集作為判斷它是不是某個(gè)類的實(shí)例的唯一標(biāo)準(zhǔn)。這種技術(shù)在檢查一個(gè)類是否實(shí)現(xiàn)了某個(gè)接口時(shí)也可以大顯身手。這種方法的背后觀點(diǎn)很簡(jiǎn)單:如果對(duì)象具有與接口定義的方法同名的所有方法,那么就可以認(rèn)為它實(shí)現(xiàn)了這個(gè)接口。可以使用一個(gè)輔助函數(shù)來(lái)確保對(duì)象具有所有必需的方法,代碼如下:
- //interface
- var Composite =new Interface('Composite',['add','remove','getName']);
- var FormItem=new Interface('FormItem',['save']);
- //class
- var Composite=function(id,method,action){
- }
- //Common Method
- function AddForm(formInstance){
- ensureImplements(formInstance,Composite,FormItem);
- //如果該函數(shù)沒有實(shí)現(xiàn)指定的接口,這個(gè)函數(shù)將會(huì)報(bào)錯(cuò)
- }
與另外兩種方式不同,這種方式無(wú)需注釋,其余的各個(gè)方面都是可以強(qiáng)制實(shí)施的。EnsureImplements函數(shù)需要至少兩個(gè)參數(shù)。***個(gè)參數(shù)是想要檢查的對(duì)象,其余的參數(shù)是被檢查對(duì)象的接口。該函數(shù)檢查器***個(gè)參數(shù)代表的對(duì)象是否實(shí)現(xiàn)了那些接口所聲明的方法,如果漏掉了任何一個(gè),就會(huì)拋錯(cuò),其中會(huì)包含被遺漏的方法的有效信息。這種方式不具備自我描述性,需要一個(gè)輔助類和輔助函數(shù)來(lái)幫助實(shí)現(xiàn)接口檢查,而且它只關(guān)心方法名稱,并不檢查參數(shù)的名稱、數(shù)目或類型。
四、Interface類
在下面的代碼中,對(duì)Interface類的所有方法的參數(shù)都進(jìn)行了嚴(yán)格的控制,如果參數(shù)沒有驗(yàn)證通過(guò),那么就會(huì)拋出異常。加入這種檢查的目的就是,如果在執(zhí)行過(guò)程中沒有拋出異常,那么就可以肯定接口得到了正確的聲明和實(shí)現(xiàn)。
- var Interface = function(name ,methods){
- if(arguments.length!=2){
- throw new Error('2 arguments required!');
- }
- this.name=name;
- this.methods=[];
- for(var i=0;len=methods.length;i<len;i++){
- if(typeof(methods[i]!=='String')){
- throw new Error('method name must be String!');
- }
- this.methods.push(methods[i]);
- }
- }
- Interface.ensureImplements=function(object){
- if(arguments.length<2){
- throw new Error('2 arguments required at least!');
- }
- for(var i=0;len=arguments.length;i<len;i++){
- var interface=arguments[i];
- if(interface.constructor!==Interface){
- throw new Error('instance must be Interface!');
- }
- for(var j=0;methodLength=interface.methods.length;j<methodLength;j++){
- var method=interface.methods[j];
- if(!object[method]||typeof(object[method])=='function')){
- throw new Error('object does not implements method!');
- }
- }
- }
- }
其實(shí)多數(shù)情況下,接口并不是經(jīng)常被使用的,嚴(yán)格的類型檢查并不總是明智的。但是在設(shè)計(jì)復(fù)雜的系統(tǒng)的時(shí)候,接口的作用就體現(xiàn)出來(lái)了,這看似降低了靈活性,卻同時(shí)也降低了耦合性,提高了代碼的重用性。這在大型系統(tǒng)中是比較有優(yōu)勢(shì)的。在下面的例子中,聲明了一個(gè)displayRoute方法,要求其參數(shù)具有三個(gè)特定的方法,通過(guò)Interface對(duì)象和ensureImplements方法來(lái)保證這三個(gè)方法的實(shí)現(xiàn),否則將會(huì)拋出錯(cuò)誤。
- //聲明一個(gè)接口,描述該接口包含的方法
- var DynamicMap=new Interface{'DynamicMap',['centerOnPoint','zoom','draw']};
- //聲明一個(gè)displayRoute方法
- function displayRoute(mapInstance){
- //檢驗(yàn)該方法的map
- //檢驗(yàn)該方法的mapInsstance是否實(shí)現(xiàn)了DynamicMap接口,如果未實(shí)現(xiàn)則會(huì)拋出
- Interface.ensureImplements(mapInstance,DynamicMap);
- //如果實(shí)現(xiàn)了則正常執(zhí)行
- mapInstance.centerOnPoint(12,22);
- mapInstance.zoom(5);
- mapInstance.draw();
- }
下面的例子會(huì)將一些數(shù)據(jù)以網(wǎng)頁(yè)的形式展現(xiàn)出來(lái),這個(gè)類的構(gòu)造器以一個(gè)TestResult的實(shí)例作為參數(shù)。該類會(huì)對(duì)TestResult對(duì)象所包含的數(shù)據(jù)進(jìn)行格式化(Format)后輸出,代碼如下:
- var ResultFormatter=function(resultObject){
- //對(duì)resultObject進(jìn)行檢查,保證是TestResult的實(shí)例
- if(!(resultObject instanceof TestResult)){
- throw new Error('arguments error!');
- }
- this.resultObject=resultObject;
- }
- ResultFormatter.prototype.renderResult=function(){
- var dateOfTest=this.resultObject.getData();
- var resultArray=this.resultObject.getResults();
- var resultContainer=document.createElement('div');
- var resultHeader=document.createElement('h3');
- resultHeader.innerHTML='Test Result from '+dateOfTest.toUTCString();
- resultContainer.appendChild(resultHeader);
- var resultList=document.createElement('ul');
- resultContainer.appendChild(resultList);
- for(var i=0;len=resultArray.length;i<len;i++){
- var listItem=document.createElement('li');
- listItem.innerHTML=resultArray[i];
- resultList.appendChild('listItem');
- }
- return resultContainer;
- }
該類的構(gòu)造器會(huì)對(duì)參數(shù)進(jìn)行檢查,以確保其的確為TestResult的類的實(shí)例。如果參數(shù)達(dá)不到要求,構(gòu)造器將會(huì)拋出一個(gè)錯(cuò)誤。有了這樣的保證,在編寫renderResult方法的時(shí)候,就可以認(rèn)定有g(shù)etData和getResult兩個(gè)方法。但是,構(gòu)造函數(shù)中,只對(duì)參數(shù)的類型進(jìn)行了檢查,實(shí)際上這并不能保證所需要的方法都得到了實(shí)現(xiàn)。TestResult類會(huì)被修改,致使其失去這兩個(gè)方法,但是構(gòu)造器中的檢查依舊會(huì)通過(guò),只是renderResult方法不再有效。
此外,構(gòu)造器中的這個(gè)檢查施加了一些不必要的限制。它不允許使用其他的類的實(shí)例作為參數(shù),否則會(huì)直接拋錯(cuò),但是問(wèn)題來(lái)了,如果有另一個(gè)類也包含并實(shí)現(xiàn)了getData和getResult方法,它本來(lái)可以被ResultFormatter使用,卻因?yàn)檫@個(gè)限制而無(wú)用武之地。
解決問(wèn)題的辦法就是刪除構(gòu)造器中的校驗(yàn),并使用接口代替。我們采用這個(gè)方案對(duì)代碼進(jìn)行優(yōu)化:
- //接口的聲明
- var resultSet =new Interface('ResultSet',['getData','getResult']);
- //修改后的方案
- var ResultFormatter =function(resultObject){
- Interface.ensureImplements(resultObject,resultSet);
- this.resultObject=resultObject;
- }
上述代碼中,renderResult方法保持不變,而構(gòu)造器卻采用的ensureImplements方法,而不是typeof運(yùn)算符?,F(xiàn)在的這個(gè)構(gòu)造器可以接受任何符合接口的類的實(shí)例了。
五、依賴于接口的設(shè)計(jì)模式
<1>工廠模式:對(duì)象工廠所創(chuàng)建的具體對(duì)象會(huì)因具體情況而不同。使用接口可以確保所創(chuàng)建的這些對(duì)象可以互換使用,也就是說(shuō)對(duì)象工廠可以保證其生產(chǎn)出來(lái)的對(duì)象都實(shí)現(xiàn)了必需的方法;
<2>組合模式:如果不使用接口就不可能使用這個(gè)模式,其中心思想是可以將對(duì)象群體與其組成對(duì)象同等對(duì)待。這是通過(guò)接口來(lái)做到的。如果不進(jìn)行鴨式辯型或類型檢查,那么組合模式就會(huì)失去大部分意義;
<3>裝飾者模式:裝飾者通過(guò)透明地為另一個(gè)對(duì)象提供包裝而發(fā)揮作用。這是通過(guò)實(shí)現(xiàn)與另外那個(gè)對(duì)象完全一致的接口實(shí)現(xiàn)的。對(duì)于外界而言,一個(gè)裝飾者和它所包裝的對(duì)象看不出有什么區(qū)別,所以使用Interface來(lái)確保所創(chuàng)建的裝飾者實(shí)現(xiàn)了必需的方法;
<4>命令模式:代碼中所有的命令對(duì)象都有實(shí)現(xiàn)同一批方法(如run、ecxute、do等)通過(guò)使用接口,未執(zhí)行這些命令對(duì)象而創(chuàng)建的類可以不必知道這些對(duì)象具體是什么,只要知道他們都正確地實(shí)現(xiàn)了接口即可。借此可以創(chuàng)建出模塊化程度很高的、耦合度很低的API。