1. 程式人生 > >面向接口編程

面向接口編程

animal 主動 裏的 開發文檔 詳細介紹 scrip abs 單繼承 上轉型

前面的話

  談到接口的時候,通常會涉及以下幾種含義。經常說一個庫或者模塊對外提供了某某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();

面向接口編程