談到接口的時候,一般會涉及如下幾種含義。常常說一個庫或者模塊對外提供了某某API接口。經過主動暴露的接口來通訊,能夠隱藏軟件系統內部的工做細節。這也是最熟悉的第一種接口含義。第二種接口是一些語言提供的關鍵字,好比Java的interface。interface關鍵字能夠產生一個徹底抽象的類。這個徹底抽象的類用來表示一種契約,專門負責創建類與類之間的聯繫。第三種接口便是談論的「面向接口編程」中的接口,接口是對象能響應的請求的集合。本文將詳細介紹面向接口編程javascript
由於javascript並無從語言層面提供對抽象類(Abstractclass)或者接口(interface)的支持,有必要從一門提供了抽象類和接口的語言開始,逐步瞭解「面向接口編程」在面向對象程序設計中的做用html
有一個鴨子類Duck,還有一個讓鴨子發出叫聲的AnimalSound類,該類有一個makeSound方法,接收Duck類型的對象做爲參數,代碼以下:java
public class Duck { // 鴨子類 public void makeSound(){ System.out.println( "嘎嘎嘎" ); } } public class AnimalSound { public void makeSound( Duck duck ){ // (1) 只接受 Duck 類型的參數 duck.makeSound(); } } public class Test { public static void main( String args[] ){ AnimalSound animalSound = new AnimalSound(); Duck duck = new Duck(); animalSound.makeSound( duck ); // 輸出:嘎嘎嘎 } }
目前已經能夠順利地讓鴨子發出叫聲。後來動物世界裏又增長了一些雞,如今想讓雞也叫喚起來,但發現這是一件不可能完成的事情,由於在上面這段代碼的(1)處,即AnimalSound類的sound方法裏,被規定只能接受Duck類型的對象做爲參數:程序員
public class Chicken { // 雞類 public void makeSound(){ System.out.println( "咯咯咯" ); } } public class Test { public static void main( String args[] ){ AnimalSound animalSound = new AnimalSound(); Chicken chicken = new Chicken(); animalSound.makeSound( chicken ); // 報錯,animalSound.makeSound 只能接受 Duck 類型的參數 } }
在享受靜態語言類型檢查帶來的安全性的同時,也失去了一些編寫代碼的自由編程
靜態類型語言一般設計爲能夠「向上轉型」。當給一個類變量賦值時,這個變量的類型既可使用這個類自己,也可使用這個類的超類。就像看到天上有隻麻雀,既能夠說「一隻麻雀在飛」,也能夠說「一隻鳥在飛」,甚至能夠說成「一隻動物在飛」。經過向上轉型,對象的具體類型被隱藏在「超類型」身後。當對象類型之間的耦合關係被解除以後,這些對象才能在類型檢查系統的監視下相互替換使用,這樣才能看到對象的多態性設計模式
因此若是想讓雞也叫喚起來,必須先把duck對象和chicken對象都向上轉型爲它們的超類型Animal類,進行向上轉型的工具就是抽象類或者interface。即將使用的是抽象類。先建立一個Animal抽象類:數組
public abstract class Animal{ abstract void makeSound(); //抽象方法 }
而後讓Duck類和Chicken類都繼承自抽象類Animal:安全
public class Chicken extends Animal{ public void makeSound(){ System.out.println( "咯咯咯" ); } } public class Duck extends Animal{ public void makeSound(){ System.out.println( "嘎嘎嘎" ); } }
也能夠把Animal定義爲一個具體類而不是抽象類,但通常不這麼作。如今剩下的就是讓AnimalSound類的makeSound方法接收Animal類型的參數,而不是具體的Duck類型或者Chicken類型:閉包
public class AnimalSound{ public void makeSound( Animal animal ){ // 接收 Animal 類型的參數,而非 Duck 類型或 Chicken 類型 animal.makeSound(); } } public class Test { public static void main( String args[] ){ AnimalSound animalSound = new AnimalSound (); Animal duck = new Duck(); // 向上轉型 Animal chicken = new Chicken(); // 向上轉型 animalSound.makeSound( duck ); // 輸出:嘎嘎嘎 animalSound.makeSound( chicken ); // 輸出:咯咯咯 } }
抽象類在這裏主要有如下兩個做用編程語言
一、向上轉型。讓Duck對象和Chicken對象的類型都隱藏在Animal類型身後,隱藏對象的具體類型以後,duck對象和chicken對象才能被交換使用,這是讓對象表現出多態性的必經之路
二、創建一些契約。繼承自抽象類的具體類都會繼承抽象類裏的abstract方法,而且要求覆寫它們。這些契約在實際編程中很是重要,能夠幫助編寫可靠性更高的代碼。好比在命令模式中,各個子命令類都必須實現execute方法,才能保證在調用command.execute的時候不會拋出異常。若是讓子命令類OpenTvCommand繼承自抽象類Command:
abstract class Command{ public abstract void execute(); } public class OpenTvCommand extends Command{ public OpenTvCommand (){}; public void execute(){ System.out.println( "打開電視機" ); } }
天然有編譯器幫助檢查和保證子命令類OpenTvCommand覆寫了抽象類Command中的execute抽象方法。若是沒有這樣作,編譯器會盡量早地拋出錯誤來提醒正在編寫這段代碼的程序員
總而言之,不關注對象的具體類型,而僅僅針對超類型中的「契約方法」來編寫程序,能夠產生可靠性高的程序,也能夠極大地減小子系統實現之間的相互依賴關係,這就是面向接口編程
從過程上來看,「面向接口編程」實際上是「面向超類型編程」。當對象的具體類型被隱藏在超類型身後時,這些對象就能夠相互替換使用,關注點才能從對象的類型上轉移到對象的行爲上。「面向接口編程」也能夠當作面向抽象編程,即針對超類型中的abstract方法編程,接口在這裏被當成abstract方法中約定的契約行爲。這些契約行爲暴露了一個類或者對象可以作什麼,可是不關心具體如何去作
除了用抽象類來完成面向接口編程以外,使用interface也能夠達到一樣的效果。雖然不少人在實際使用中刻意區分抽象類和interface,但使用interface實際上也是繼承的一種方式,叫做接口繼承
相對於單繼承的抽象類,一個類能夠實現多個interface。抽象類中除了abstract方法以外,還能夠有一些供子類公用的具體方法。interface使抽象的概念更進一步,它產生一個徹底抽象的類,不提供任何具體實現和方法體,但容許該interface的建立者肯定方法名、參數列表和返回類型,這至關於提供一些行爲上的約定,但不關心該行爲的具體實現過程。interface一樣能夠用於向上轉型,這也是讓對象表現出多態性的一條途徑,實現了同一個接口的兩個類就能夠被相互替換使用
再回到用抽象類實現讓鴨子和雞發出叫聲的故事。這個故事得以完美收場的關鍵是讓抽象類Animal給duck和chicken進行向上轉型。但此時也引入了一個限制,抽象類是基於單繼承的,也就是說不可能讓Duck和Chicken再繼承自另外一個家禽類。若是使用interface,能夠僅僅針對發出叫聲這個行爲來編寫程序,同時一個類也能夠實現多個interface
下面用interface來改寫基於抽象類的代碼。先定義Animal接口,全部實現了Animal接口的動物類都將擁有Animal接口中約定的行爲:
public interface Animal{ abstract void makeSound(); } public class Duck implements Animal{ public void makeSound() { // 重寫 Animal 接口的 makeSound 抽象方法 System.out.println( "嘎嘎嘎" ); } } public class Chicken implements Animal{ public void makeSound() { // 重寫 Animal 接口的 makeSound 抽象方法 System.out.println( "咯咯咯" ); } } public class AnimalSound { public void makeSound( Animal animal ){ animal.makeSound(); } } public class Test { public static void main( String args[] ){ Animal duck = new Duck(); Animal chicken = new Chicken(); AnimalSound animalSound = new AnimalSound(); animalSound.makeSound( duck ); // 輸出:嘎嘎嘎 animalSound.makeSound( chicken ); // 輸出:咯咯咯 } }
由於javascript是一門動態類型語言,類型自己在javascript中是一個相對模糊的概念。也就是說,不須要利用抽象類或者interface給對象進行「向上轉型」。除了number、string、boolean等基本數據類型以外,其餘的對象均可以被當作「天生」被「向上轉型」成了Object類型:
var ary = new Array(); var date = new Date();
若是javascript是一門靜態類型語言,上面的代碼也許能夠理解爲:
Array ary = new Array(); Date date = new Date();
或者:
Object ary = new Array(); Object date = new Date();
不多有人在javascript開發中去關心對象的真正類型。在動態類型語言中,對象的多態性是與生俱來的,但在另一些靜態類型語言中,對象類型之間的解耦很是重要,甚至有一些設計模式的主要目的就是專門隱藏對象的真正類型
由於不須要進行向上轉型,接口在javascript中的最大做用就退化到了檢查代碼的規範性。好比檢查某個對象是否實現了某個方法,或者檢查是否給函數傳入了預期類型的參數。若是忽略了這兩點,有可能會在代碼中留下一些隱藏的bug。好比嘗試執行obj對象的show方法,可是obj對象自己卻沒有實現這個方法,代碼以下:
function show( obj ){ obj.show(); // Uncaught TypeError: undefined is not a function } var myObject = {}; // myObject 對象沒有 show 方法 show( myObject ); 或者: function show( obj ){ obj.show(); // TypeError: number is not a function } var myObject = { // myObject.show 不是 Function 類型 show: 1 }; show( myObject );
此時,不得不加上一些防護性代碼:
function show( obj ){ if ( obj && typeof obj.show === 'function' ){ obj.show(); } }
或者:
function show( obj ){ try{ obj.show(); }catch( e ){ } } var myObject = {}; // myObject 對象沒有 show 方法 // var myObject = { // myObject.show 不是 Function 類型 // show: 1 // }; show( myObject );
若是javascript有編譯器幫助檢查代碼的規範性,那事情要比如今美好得多,不用在業務代碼中處處插入一些跟業務邏輯無關的防護性代碼。做爲一門解釋執行的動態類型語言,把但願寄託在編譯器上是不可能了。若是要處理這類異常狀況,只有手動編寫一些接口檢查的代碼
【接口檢查】
鴨子類型是動態類型語言面向對象設計中的一個重要概念。利用鴨子類型的思想,沒必要藉助超類型的幫助,就能在動態類型語言中輕鬆地實現面向接口編程。好比,一個對象若是有push和pop方法,而且提供了正確的實現,它就能被看成棧來使用;一個對象若是有length屬性,也能夠依照下標來存取屬性,這個對象就能夠被看成數組來使用。若是兩個對象擁有相同的方法,則有很大的可能性它們能夠被相互替換使用
在Object.prototype.toString.call([])==='[object Array]'被發現以前,常常用鴨子類型的思想來判斷一個對象是不是一個數組,代碼以下:
var isArray = function( obj ){ return obj && typeof obj === 'object' && typeof obj.length === 'number' && typeof obj.splice === 'function' };
固然在javascript開發中,老是進行接口檢查是不明智的,也是沒有必要的,畢竟如今還找不到一種好用而且通用的方式來模擬接口檢查,跟業務邏輯無關的接口檢查也會讓不少javascript程序員以爲不值得和不習慣
雖然在大多數時候interface給javascript開發帶來的價值並不像在靜態類型語言中那麼大,但若是正在編寫一個複雜的應用,仍是會常常懷念接口的幫助。下面以基於命令模式的示例來講明interface如何規範程序員的代碼編寫,這段代碼自己並無什麼實用價值,在javascript中,通常用閉包和高階函數來實現命令模式
假設正在編寫一個用戶界面程序,頁面中有成百上千個子菜單。由於項目很複雜,決定讓整個程序都基於命令模式來編寫,即編寫菜單集合界面的是某個程序員,而負責實現每一個子菜單具體功能的工做交給了另一些程序員。那些負責實現子菜單功能的程序員,在完成本身的工做以後,會把子菜單封裝成一個命令對象,而後把這個命令對象交給編寫菜單集合界面的程序員。已經約定好,當調用子菜單對象的execute方法時,會執行對應的子菜單命令。雖然在開發文檔中詳細註明了每一個子菜單對象都必須有本身的execute方法,但仍是有一個粗心的javascript程序員忘記給他負責的子菜單對象實現execute方法,因而當執行這個命令的時候,便會報出錯誤,代碼以下:
<html> <body> <button id="exeCommand">執行菜單命令</button> <script> var RefreshMenuBarCommand = function(){}; RefreshMenuBarCommand.prototype.execute = function(){ console.log( '刷新菜單界面' ); }; var AddSubMenuCommand = function(){}; AddSubMenuCommand.prototype.execute = function(){ console.log( '增長子菜單' ); }; var DelSubMenuCommand = function(){}; /*****沒有實現DelSubMenuCommand.prototype.execute *****/ // DelSubMenuCommand.prototype.execute = function(){ // }; var refreshMenuBarCommand = new RefreshMenuBarCommand(), addSubMenuCommand = new AddSubMenuCommand(), delSubMenuCommand = new DelSubMenuCommand(); var setCommand = function( command ){ document.getElementById( 'exeCommand' ).onclick = function(){ command.execute(); } }; setCommand( refreshMenuBarCommand ); // 點擊按鈕後輸出:"刷新菜單界面" setCommand( addSubMenuCommand ); // 點擊按鈕後輸出:"增長子菜單" setCommand( delSubMenuCommand ); // 點擊按鈕後報錯。Uncaught TypeError: undefined is not a function </script> </body> </html>
爲了防止粗心的程序員忘記給某個子命令對象實現execute方法,只能在高層函數裏添加一些防護性的代碼,這樣當程序在最終被執行的時候,有可能拋出異常來提醒咱們,代碼以下
var setCommand = function( command ){ document.getElementById( 'exeCommand' ).onclick = function(){ if ( typeof command.execute !== 'function' ){ throw new Error( "command 對象必須實現execute 方法" ); } command.execute(); } };
若是確實不喜歡重複編寫這些防護性代碼,還能夠嘗試使用TypeScript來編寫這個程序。TypeScript是微軟開發的一種編程語言,是javascript的一個超集。跟CoffeeScript相似,TypeScript代碼最終會被編譯成原生的javascript代碼執行。經過TypeScript,可使用靜態語言的方式來編寫javascript程序。用TypeScript來實現一些設計模式,顯得更加原汁原味。TypeScript目前的版本尚未提供對抽象類的支持,可是提供了interface。下面就來編寫一個TypeScript版本的命令模式
首先定義Command接口:
interface Command{ execute:Function; }
接下來定義RefreshMenuBarCommand、AddSubMenuCommand和DelSubMenuCommand這3個類,它們分別都實現了Command接口,這能夠保證它們都擁有execute方法:
class RefreshMenuBarCommand implements Command{ constructor (){ } execute(){ console.log( '刷新菜單界面' ); } } class AddSubMenuCommand implements Command{ constructor (){ } execute(){ console.log( '增長子菜單' ); } } class DelSubMenuCommand implements Command{ constructor (){ } // 忘記重寫execute 方法 } var refreshMenuBarCommand = new RefreshMenuBarCommand(), addSubMenuCommand = new AddSubMenuCommand(), delSubMenuCommand = new DelSubMenuCommand(); refreshMenuBarCommand.execute(); // 輸出:刷新菜單界面 addSubMenuCommand.execute(); // 輸出:增長子菜單 delSubMenuCommand.execute(); // 輸出:Uncaught TypeError: undefined is not a function
忘記在DelSubMenuCommand類中重寫execute方法時,TypeScript提供的編譯器及時給出了錯誤提示
這段TypeScript代碼翻譯過來的javascript代碼以下:
var RefreshMenuBarCommand = (function () { function RefreshMenuBarCommand() {} RefreshMenuBarCommand.prototype.execute = function () { console.log('刷新菜單界面'); }; return RefreshMenuBarCommand; })(); var AddSubMenuCommand = (function () { function AddSubMenuCommand() {} AddSubMenuCommand.prototype.execute = function () { console.log('增長子菜單'); }; return AddSubMenuCommand; })(); var DelSubMenuCommand = (function () { function DelSubMenuCommand() {} return DelSubMenuCommand; })(); var refreshMenuBarCommand = new RefreshMenuBarCommand(), addSubMenuCommand = new AddSubMenuCommand(), delSubMenuCommand = new DelSubMenuCommand(); refreshMenuBarCommand.execute(); addSubMenuCommand.execute(); delSubMenuCommand.execute();