官术网_书友最值得收藏!

1.2 多態

“多態”一詞源于希臘文polymorphism,拆開來看是poly(復數)+ morph(形態)+ ism,從字面上我們可以理解為復數形態。

多態的實際含義是:同一操作作用于不同的對象上面,可以產生不同的解釋和不同的執行結果。換句話說,給不同的對象發送同一個消息的時候,這些對象會根據這個消息分別給出不同的反饋。

從字面上來理解多態不太容易,下面我們來舉例說明一下。

主人家里養了兩只動物,分別是一只鴨和一只雞,當主人向它們發出“叫”的命令時,鴨會“嘎嘎嘎”地叫,而雞會“咯咯咯”地叫。這兩只動物都會以自己的方式來發出叫聲。它們同樣“都是動物,并且可以發出叫聲”,但根據主人的指令,它們會各自發出不同的叫聲。

其實,其中就蘊含了多態的思想。下面我們通過代碼進行具體的介紹。

1.2.1 一段“多態”的JavaScript代碼

我們把上面的故事用JavaScript代碼實現如下:

        var makeSound = function( animal ){
            if ( animal instanceof Duck ){
              console.log( ’嘎嘎嘎’ );
            }else if ( animal instanceof Chicken ){
              console.log( ’咯咯咯’ );
            }
        };

        var Duck = function(){};
        var Chicken = function(){};

        makeSound( new Duck() );      // 嘎嘎嘎
        makeSound( new Chicken() );   // 咯咯咯

這段代碼確實體現了“多態性”,當我們分別向鴨和雞發出“叫喚”的消息時,它們根據此消息作出了各自不同的反應。但這樣的“多態性”是無法令人滿意的,如果后來又增加了一只動物,比如狗,顯然狗的叫聲是“汪汪汪”,此時我們必須得改動makeSound函數,才能讓狗也發出叫聲。修改代碼總是危險的,修改的地方越多,程序出錯的可能性就越大,而且當動物的種類越來越多時,makeSound有可能變成一個巨大的函數。

多態背后的思想是將“做什么”和“誰去做以及怎樣去做”分離開來,也就是將“不變的事物”與“可能改變的事物”分離開來。在這個故事中,動物都會叫,這是不變的,但是不同類型的動物具體怎么叫是可變的。把不變的部分隔離出來,把可變的部分封裝起來,這給予了我們擴展程序的能力,程序看起來是可生長的,也是符合開放—封閉原則的,相對于修改代碼來說,僅僅增加代碼就能完成同樣的功能,這顯然優雅和安全得多。

1.2.2 對象的多態性

下面是改寫后的代碼,首先我們把不變的部分隔離出來,那就是所有的動物都會發出叫聲:

        var makeSound = function( animal ){
            animal.sound();
        };

然后把可變的部分各自封裝起來,我們剛才談到的多態性實際上指的是對象的多態性:

        var Duck = function(){}
        Duck.prototype.sound = function(){
            console.log( ’嘎嘎嘎’ );
        };

        var Chicken = function(){}
        Chicken.prototype.sound = function(){
            console.log( ’咯咯咯’ );
        };

        makeSound( new Duck() );        //嘎嘎嘎
        makeSound( new Chicken() );     //咯咯咯

現在我們向鴨和雞都發出“叫喚”的消息,它們接到消息后分別作出了不同的反應。如果有一天動物世界里又增加了一只狗,這時候只要簡單地追加一些代碼就可以了,而不用改動以前的makeSound函數,如下所示:

        varDog=function(){}

        Dog.prototype.sound=function(){
            console.log(’汪汪汪’);
        };

        makeSound(newDog());        //汪汪汪

1.2.3 類型檢查和多態

類型檢查是在表現出對象多態性之前的一個繞不開的話題,但JavaScript是一門不必進行類型檢查的動態類型語言,為了真正了解多態的目的,我們需要轉一個彎,從一門靜態類型語言說起。

我們在1.1節已經說明過靜態類型語言在編譯時會進行類型匹配檢查。以Java為例,由于在代碼編譯時要進行嚴格的類型檢查,所以不能給變量賦予不同類型的值,這種類型檢查有時候會讓代碼顯得僵硬,代碼如下:

        String str;

        str = "abc";    //沒有問題
        str=2;    //報錯

現在我們嘗試把上面讓鴨子和雞叫喚的例子換成Java代碼:

        public class Duck {        //鴨子類
            public void makeSound(){
              System.out.println( "嘎嘎嘎" );
            }
        }
        public class Chicken {        // 雞類
            public void makeSound(){
              System.out.println( "咯咯咯" );
            }
        }

        public class AnimalSound {
            public void makeSound( Duck duck ){    // (1)
              duck.makeSound();
            }

        }

        public class Test {
            public static void main( String args[] ){
              AnimalSound animalSound = new AnimalSound();
              Duck duck = new Duck();
              animalSound.makeSound( duck );    // 輸出:嘎嘎嘎
            }
        }

我們已經順利地讓鴨子可以發出叫聲,但如果現在想讓雞也叫喚起來,我們發現這是一件不可能實現的事情。因為(1)處AnimalSound類的makeSound方法,被我們規定為只能接受Duck類型的參數:

        public class Test {
            public static void main( String args[] ){
              AnimalSound animalSound = new AnimalSound();
              Chicken chicken = new Chicken();
              animalSound.makeSound( chicken );    // 報錯,只能接受Duck類型的參數
            }
        }

某些時候,在享受靜態語言類型檢查帶來的安全性的同時,我們亦會感覺被束縛住了手腳。

為了解決這一問題,靜態類型的面向對象語言通常被設計為可以向上轉型:當給一個類變量賦值時,這個變量的類型既可以使用這個類本身,也可以使用這個類的超類。這就像我們在描述天上的一只麻雀或者一只喜鵲時,通常說“一只麻雀在飛”或者“一只喜鵲在飛”。但如果想忽略它們的具體類型,那么也可以說“一只鳥在飛”。

同理,當Duck對象和Chicken對象的類型都被隱藏在超類型Animal身后,Duck對象和Chicken對象就能被交換使用,這是讓對象表現出多態性的必經之路,而多態性的表現正是實現眾多設計模式的目標。

1.2.4 使用繼承得到多態效果

使用繼承來得到多態效果,是讓對象表現出多態性的最常用手段。繼承通常包括實現繼承和接口繼承。本節我們討論實現繼承,接口繼承的例子請參見第21章。

我們先創建一個Animal抽象類,再分別讓Duck和Chicken都繼承自Animal抽象類,下述代碼中(1)處和(2)處的賦值語句顯然是成立的,因為鴨子和雞也是動物:

        public abstract class Animal {
            abstract void makeSound();   //抽象方法
        }

        public class Chicken extends Animal{
            public void makeSound(){
              System.out.println( "咯咯咯" );
            }
        }

        public class Duck extends Animal{
            public void makeSound(){
              System.out.println( "嘎嘎嘎" );
            }
        }

        Animal duck = new Duck();       // (1)
        Animal chicken = new Chicken();    // (2)

現在剩下的就是讓AnimalSound類的makeSound方法接受Animal類型的參數,而不是具體的Duck類型或者Chicken類型:

        publicclassAnimalSound{
            public void makeSound( Animal animal ){    //接受Animal類型的參數
              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.2.5 JavaScript的多態

從前面的講解我們得知,多態的思想實際上是把“做什么”和“誰去做”分離開來,要實現這一點,歸根結底先要消除類型之間的耦合關系。如果類型之間的耦合關系沒有被消除,那么我們在makeSound方法中指定了發出叫聲的對象是某個類型,它就不可能再被替換為另外一個類型。在Java中,可以通過向上轉型來實現多態。

而JavaScript的變量類型在運行期是可變的。一個JavaScript對象,既可以表示Duck類型的對象,又可以表示Chicken類型的對象,這意味著JavaScript對象的多態性是與生俱來的。

這種與生俱來的多態性并不難解釋。JavaScript作為一門動態類型語言,它在編譯時沒有類型檢查的過程,既沒有檢查創建的對象類型,又沒有檢查傳遞的參數類型。在1.2.2節的代碼示例中,我們既可以往makeSound函數里傳遞duck對象當作參數,也可以傳遞chicken對象當作參數。

由此可見,某一種動物能否發出叫聲,只取決于它有沒有makeSound方法,而不取決于它是否是某種類型的對象,這里不存在任何程度上的“類型耦合”。這正是我們從上一節的鴨子類型中領悟的道理。在JavaScript中,并不需要諸如向上轉型之類的技術來取得多態的效果。

1.2.6 多態在面向對象程序設計中的作用

有許多人認為,多態是面向對象編程語言中最重要的技術。但我們目前還很難看出這一點,畢竟大部分人都不關心雞是怎么叫的,也不想知道鴨是怎么叫的。讓雞和鴨在同一個消息之下發出不同的叫聲,這跟程序員有什么關系呢?

Martin Fowler在《重構:改善既有代碼的設計》里寫到:

多態的最根本好處在于,你不必再向對象詢問“你是什么類型”而后根據得到的答案調用對象的某個行為——你只管調用該行為就是了,其他的一切多態機制都會為你安排妥當。

換句話說,多態最根本的作用就是通過把過程化的條件分支語句轉化為對象的多態性,從而消除這些條件分支語句。

Martin Fowler的話可以用下面這個例子很好地詮釋:

在電影的拍攝現場,當導演喊出“action”時,主角開始背臺詞,照明師負責打燈光,后面的群眾演員假裝中槍倒地,道具師往鏡頭里撒上雪花。在得到同一個消息時,每個對象都知道自己應該做什么。如果不利用對象的多態性,而是用面向過程的方式來編寫這一段代碼,那么相當于在電影開始拍攝之后,導演每次都要走到每個人的面前,確認它們的職業分工(類型),然后告訴他們要做什么。如果映射到程序中,那么程序中將充斥著條件分支語句。

利用對象的多態性,導演在發布消息時,就不必考慮各個對象接到消息后應該做什么。對象應該做什么并不是臨時決定的,而是已經事先約定和排練完畢的。每個對象應該做什么,已經成為了該對象的一個方法,被安裝在對象的內部,每個對象負責它們自己的行為。所以這些對象可以根據同一個消息,有條不紊地分別進行各自的工作。

將行為分布在各個對象中,并讓這些對象各自負責自己的行為,這正是面向對象設計的優點。

再看一個現實開發中遇到的例子,這個例子的思想和動物叫聲的故事非常相似。

假設我們要編寫一個地圖應用,現在有兩家可選的地圖API提供商供我們接入自己的應用。目前我們選擇的是谷歌地圖,谷歌地圖的API中提供了show方法,負責在頁面上展示整個地圖。示例代碼如下:

圖1-1

圖1-2

圖1-3

圖3-1

        var googleMap = {
            show: function(){
              console.log( ’開始渲染谷歌地圖’ );
            }
        };

        var renderMap = function(){
            googleMap.show();
        };

        renderMap();    //輸出開始渲染谷歌地圖

后來因為某些原因,要把谷歌地圖換成百度地圖,為了讓renderMap函數保持一定的彈性,我們用一些條件分支來讓renderMap函數同時支持谷歌地圖和百度地圖:

        var googleMap = {
            show: function(){
              console.log( ’開始渲染谷歌地圖’ );
            }
        };

        var baiduMap = {
            show: function(){
              console.log( ’開始渲染百度地圖’ );
            }
        };

        var renderMap = function( type ){
            if ( type === 'google' ){
              googleMap.show();
            }else if ( type === 'baidu' ){
              baiduMap.show();
            }
        };

        renderMap( 'google' );    //輸出:開始渲染谷歌地圖
        renderMap( 'baidu' );     //輸出:開始渲染百度地圖

可以看到,雖然renderMap函數目前保持了一定的彈性,但這種彈性是很脆弱的,一旦需要替換成搜搜地圖,那無疑必須得改動renderMap函數,繼續往里面堆砌條件分支語句。

我們還是先把程序中相同的部分抽象出來,那就是顯示某個地圖:

        var renderMap = function( map ){
            if ( map.show instanceof Function ){
              map.show();
            }
        };
        renderMap( googleMap );    //輸出:開始渲染谷歌地圖
        renderMap( baiduMap );     //輸出:開始渲染百度地圖

現在來找找這段代碼中的多態性。當我們向谷歌地圖對象和百度地圖對象分別發出“展示地圖”的消息時,會分別調用它們的show方法,就會產生各自不同的執行結果。對象的多態性提示我們,“做什么”和“怎么去做”是可以分開的,即使以后增加了搜搜地圖,renderMap函數仍然不需要做任何改變,如下所示:

        var sosoMap = {
            show: function(){
              console.log( '開始渲染搜搜地圖' );
            }
        };

        renderMap( sosoMap );     //輸出:開始渲染搜搜地圖

在這個例子中,我們假設每個地圖API提供展示地圖的方法名都是show,在實際開發中也許不會如此順利,這時候可以借助適配器模式來解決問題。

1.2.7 設計模式與多態

GoF所著的《設計模式》一書的副書名是“可復用面向對象軟件的基礎”。該書完全是從面向對象設計的角度出發的,通過對封裝、繼承、多態、組合等技術的反復使用,提煉出一些可重復使用的面向對象設計技巧。而多態在其中又是重中之重,絕大部分設計模式的實現都離不開多態性的思想。

拿命令模式參見第9章。來說,請求被封裝在一些命令對象中,這使得命令的調用者和命令的接收者可以完全解耦開來,當調用命令的execute方法時,不同的命令會做不同的事情,從而會產生不同的執行結果。而做這些事情的過程是早已被封裝在命令對象內部的,作為調用命令的客戶,根本不必去關心命令執行的具體過程。

在組合模式參見第10章。中,多態性使得客戶可以完全忽略組合對象和葉節點對象之間的區別,這正是組合模式最大的作用所在。對組合對象和葉節點對象發出同一個消息的時候,它們會各自做自己應該做的事情,組合對象把消息繼續轉發給下面的葉節點對象,葉節點對象則會對這些消息作出真實的反饋。

在策略模式參見第5章。中,Context并沒有執行算法的能力,而是把這個職責委托給了某個策略對象。每個策略對象負責的算法已被各自封裝在對象內部。當我們對這些策略對象發出“計算”的消息時,它們會返回各自不同的計算結果。

在JavaScript這種將函數作為一等對象的語言中,函數本身也是對象,函數用來封裝行為并且能夠被四處傳遞。當我們對一些函數發出“調用”的消息時,這些函數會返回不同的執行結果,這是“多態性”的一種體現,也是很多設計模式在JavaScript中可以用高階函數來代替實現的原因。

主站蜘蛛池模板: 浮梁县| 杭州市| 铅山县| 依兰县| 太仆寺旗| 镇远县| 巧家县| 明水县| 藁城市| 古蔺县| 承德县| 伊宁县| 马龙县| 广丰县| 克山县| 江油市| 花莲县| 全南县| 达孜县| 海淀区| 循化| 丰县| 龙陵县| 云梦县| 汽车| 铁岭县| 江北区| 南汇区| 竹北市| 博乐市| 彭山县| 铁岭市| 通榆县| 昔阳县| 无极县| 集贤县| 迭部县| 双峰县| 天全县| 平武县| 贵州省|