首頁  >  文章  >  web前端  >  學習JavaScript設計模式(多型)_javascript技巧

學習JavaScript設計模式(多型)_javascript技巧

WBOY
WBOY原創
2016-05-16 15:29:451223瀏覽

多態的實際意義是:同一操作作用於不同的物件上面,可以產生不同的解釋和不同的執行結果。換句話說,給不同的物件發送同一個訊息的時候,這些物件會根據這個訊息分別給予不同的回饋。

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

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

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

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,在实际开发中也许不会如此顺利,这时候可以借助适配器模式来解决问题。

以上就是本文的全部内容,很全面,以生动的举例来帮助大家学习多态,希望大家能够真正的有所收获。

陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn