多態的實際含義是:同一操作作用於不同的對象上面,可以產生不同的解釋和不同的執行結果。換句話說,給不同的對象發送同一個消息的時候,這些對象會根據這個消息分別給出不同的反饋。
從字面上來理解多態不太容易,下面我們來舉例說明一下。
主人家裡養了兩只動物,分別是一只鴨和一只雞,當主人向它們發出“叫”的命令時,鴨會“嘎嘎嘎”地叫,而雞會“咯咯咯”地叫。這兩只動物都會以自己的方式來發出叫聲。它們同樣“都是動物,並且可以發出叫聲”,但根據主人的指令,它們會各自發出不同的叫聲。
其實,其中就蘊含了多態的思想。下面我們通過代碼進行具體的介紹。
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有可能變成一個巨大的函數。
多態背後的思想是將“做什麼”和“誰去做以及怎樣去做”分離開來,也就是將“不變的事物”與 “可能改變的事物”分離開來。在這個故事中,動物都會叫,這是不變的,但是不同類型的動物具體怎麼叫是可變的。把不變的部分隔離出來,把可變的部分封裝起來,這給予了我們擴展程序的能力,程序看起來是可生長的,也是符合開放-封閉原則的,相對於修改代碼來說,僅僅增加代碼就能完成同樣的功能,這顯然優雅和安全得多。
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函數,如下所示:
var Dog = function(){} Dog.prototype.sound = function(){ console.log( '汪汪汪' ); }; makeSound( new Dog() ); //汪汪汪
3. 類型檢查和多態
類型檢查是在表現出對象多態性之前的一個繞不開的話題,但JavaScript是一門不必進行類型檢查的動態類型語言,為了真正了解多態的目的,我們需要轉一個彎,從一門靜態類型語言說起。
靜態類型語言在編譯時會進行類型匹配檢查。以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對象就能被交換使用,這是讓對象表現出多態性的必經之路,而多態性的表現正是實現眾多設計模式的目標。
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類型:
public class AnimalSound{ 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 ); //輸出咯咯咯 } }
5. JavaScript的多態
從前面的講解我們得知,多態的思想實際上是把“做什麼”和“誰去做”分離開來,要實現這一點,歸根結底先要消除類型之間的耦合關系。如果類型之間的耦合關系沒有被消除,那麼我們在makeSound方法中指定了發出叫聲的對象是某個類型,它就不可能再被替換為另外一個類型。在Java中,可以通過向上轉型來實現多態。
而JavaScript的變量類型在運行期是可變的。一個JavaScript對象,既可以表示Duck類型的對象,又可以表示Chicken類型的對象,這意味著JavaScript對象的多態性是與生俱來的。
這種與生俱來的多態性並不難解釋。JavaScript作為一門動態類型語言,它在編譯時沒有類型檢查的過程,既沒有檢查創建的對象類型,又沒有檢查傳遞的參數類型。在2節的代碼示例中,我們既可以往makeSound函數裡傳遞duck對象當作參數,也可以傳遞chicken對象當作參數。
由此可見,某一種動物能否發出叫聲,只取決於它有沒有makeSound方法,而不取決於它是否是某種類型的對象,這裡不存在任何程度上的“類型耦合”。這正是我們從上一節的鴨子類型中領悟的道理。在JavaScript中,並不需要諸如向上轉型之類的技術來取得多態的效果。
6. 多態在面向對象程序設計中的作用
有許多人認為,多態是面向對象編程語言中最重要的技術。但我們目前還很難看出這一點,畢竟大部分人都不關心雞是怎麼叫的,也不想知道鴨是怎麼叫的。讓雞和鴨在同一個消息之下發出不同的叫聲,這跟程序員有什麼關系呢?
Martin Fowler在《重構:改善既有代碼的設計》裡寫到:
多態的最根本好處在於,你不必再向對象詢問“你是什麼類型”而後根據得到的答案調用對象的某個行為——你只管調用該行為就是了,其他的一切多態機制都會為你安排妥當。
換句話說,多態最根本的作用就是通過把過程化的條件分支語句轉化為對象的多態性,從而消除這些條件分支語句。
Martin Fowler的話可以用下面這個例子很好地诠釋:
在電影的拍攝現場,當導演喊出“action”時,主角開始背台詞,照明師負責打燈光,後面的群眾演員假裝中槍倒地,道具師往鏡頭裡撒上雪花。在得到同一個消息時,每個對象都知道自己應該做什麼。如果不利用對象的多態性,而是用面向過程的方式來編寫這一段代碼,那麼相當於在電影開始拍攝之後,導演每次都要走到每個人的面前,確認它們的職業分工(類型),然後告訴他們要做什麼。如果映射到程序中,那麼程序中將充斥著條件分支語句。
利用對象的多態性,導演在發布消息時,就不必考慮各個對象接到消息後應該做什麼。對象應該做什麼並不是臨時決定的,而是已經事先約定和排練完畢的。每個對象應該做什麼,已經成為了該對象的一個方法,被安裝在對象的內部,每個對象負責它們自己的行為。所以這些對象可以根據同一個消息,有條不紊地分別進行各自的工作。
將行為分布在各個對象中,並讓這些對象各自負責自己的行為,這正是面向對象設計的優點。
再看一個現實開發中遇到的例子,這個例子的思想和動物叫聲的故事非常相似。
假設我們要編寫一個地圖應用,現在有兩家可選的地圖API提供商供我們接入自己的應用。目前我們選擇的是谷歌地圖,谷歌地圖的API中提供了show方法,負責在頁面上展示整個地圖。示例代碼如下:
var googleMap = { show: function(){ console.log( '開始渲染google地圖' ); } }; var renderMap = function(){ googleMap.show(); }; renderMap(); // 輸出: 開始渲染google地圖
後來因為某些原因,要把谷歌地圖換成百度地圖,為了讓renderMap函數保持一定的彈性,我們用一些條件分支來讓renderMap函數同時支持谷歌地圖和百度地圖:
var googleMap = { show: function(){ console.log( '開始渲染google地圖' ); } }; var baiduMap = { show: function(){ console.log( '開始渲染baidu地圖' ); } }; var renderMap = function( type ){ if ( type === 'google' ){ googleMap.show(); }else if ( type === 'baidu' ){ baiduMap.show(); } }; renderMap( 'google' ); // 輸出: 開始渲染google地圖 renderMap( 'baidu' ); // 輸出: 開始渲染baidu地圖
可以看到,雖然renderMap函數目前保持了一定的彈性,但這種彈性是很脆弱的,一旦需要替換成搜搜地圖,那無疑必須得改動renderMap函數,繼續往裡面堆砌條件分支語句。
我們還是先把程序中相同的部分抽象出來,那就是顯示某個地圖:
var renderMap = function( map ){ if ( map.show instanceof Function ){ map.show(); } }; renderMap( googleMap ); // 輸出: 開始渲染google地圖 renderMap( baiduMap ); // 輸出: 開始渲染baidu地圖
現在來找找這段代碼中的多態性。當我們向谷歌地圖對象和百度地圖對象分別發出“展示地圖”的消息時,會分別調用它們的show方法,就會產生各自不同的執行結果。對象的多態性提示我們,“做什麼”和“怎麼去做”是可以分開的,即使以後增加了搜搜地圖,renderMap函數仍然不需要做任何改變,如下所示:
var sosoMap = { show: function(){ console.log( '開始渲染soso地圖' ); } }; renderMap( sosoMap ); // 輸出: 開始渲染soso地圖
在這個例子中,我們假設每個地圖API提供展示地圖的方法名都是show,在實際開發中也許不會如此順利,這時候可以借助適配器模式來解決問題。
以上就是本文的全部內容,很全面,以生動的舉例來幫助大家學習多態,希望大家能夠真正的有所收獲。