面向接口編程

前面的話

  談到接口的時候,一般會涉及如下幾種含義。常常說一個庫或者模塊對外提供了某某API接口。經過主動暴露的接口來通訊,能夠隱藏軟件系統內部的工做細節。這也是最熟悉的第一種接口含義。第二種接口是一些語言提供的關鍵字,好比Java的interface。interface關鍵字能夠產生一個徹底抽象的類。這個徹底抽象的類用來表示一種契約,專門負責創建類與類之間的聯繫。第三種接口便是談論的「面向接口編程」中的接口,接口是對象能響應的請求的集合。本文將詳細介紹面向接口編程javascript

 

Java抽象類

  由於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實際上也是繼承的一種方式,叫做接口繼承

  相對於單繼承的抽象類,一個類能夠實現多個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();
相關文章
相關標籤/搜索