前面的話
談到接口的時候,通常會涉及以下幾種含義。經常說一個庫或者模塊對外提供了某某API接口。通過主動暴露的接口來通信,可以隱藏軟件系統內部的工作細節。這也是最熟悉的第一種接口含義。第二種接口是一些語言提供的關鍵字,比如Java的interface。interface關鍵字可以產生一個完全抽象的類。這個完全抽象的類用來表示一種契約,專門負責建立類與類之間的聯系。第三種接口即是談論的“面向接口編程”中的接口,接口是對象能響應的請求的集合。本文將詳細介紹面向接口編程
Java抽象類
因為javascript并沒有從語言層面提供對抽象類(Abstractclass)或者接口(interface)的支持,有必要從一門提供了抽象類和接口的語言開始,逐步了解“面向接口編程”在面向對象程序設計中的作用
有一個鴨子類Duck,還有一個讓鴨子發出叫聲的AnimalSound類,該類有一個makeSound方法,接收Duck類型的對象作為參數,代碼如下:
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 ); // 輸出:咯咯咯 } }
抽象類在這里主要有以下兩個作用
1、向上轉型。讓Duck對象和Chicken對象的類型都隱藏在Animal類型身后,隱藏對象的具體類型之后,duck對象和chicken對象才能被交換使用,這是讓對象表現出多態性的必經之路
2、建立一些契約。繼承自抽象類的具體類都會繼承抽象類里的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實際上也是繼承的一種方式,叫作接口繼承
相對于單繼承的抽象類,一個類可以實現多個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是一門動態類型語言,類型本身在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程序員覺得不值得和不習慣
TypeScript
雖然在大多數時候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();
文章列表