ホームページ > 記事 > ウェブフロントエンド > JavaScriptのデザインパターン(ポリモーフィズム)を学ぶ_JavaScriptスキル
ポリモーフィズムの実際の意味は、同じ操作を異なるオブジェクトに適用すると、異なる解釈と異なる実行結果が生成される可能性があるということです。言い換えれば、同じメッセージが異なるオブジェクトに送信されると、これらのオブジェクトはメッセージに基づいて異なるフィードバックを返します。
ポリモーフィズムを文字通り理解するのは簡単ではありません。例 を見てみましょう。
飼い主は家にアヒルとニワトリの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 は巨大な機能になる可能性があります。
ポリモーフィズムの背後にある考え方は、「何を」を「誰がどのように行うか」から分離すること、つまり「変化しないもの」と「変化する可能性のあるもの」を分離することです。この物語では、すべての動物が吠えるのは一定ですが、さまざまな種類の動物の具体的な名前は変わります。定数部分を分離し、可変部分をカプセル化すると、プログラムは拡張可能であり、オープンクローズの原則に準拠しているように見えます。コードを変更する場合と比較すると、同じ機能を実現できるコードを追加するだけです。明らかに、はるかにエレガントで安全です。
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 を例に挙げると、コードのコンパイル時に厳密な型チェックが行われるため、変数に異なる型の値を割り当てることができない場合があります。コードは次のとおりです。
次に、アヒルとニワトリを擬似的に作成する上記の例を Java コードに変更してみましょう:
String str; str = abc; //没有问题 str = 2; //报错
私たちはアヒルを鳴かせることに成功しましたが、今からニワトリを鳴らしたいとしても、それは不可能であることがわかります。なぜなら、(1) の AnimalSound クラスの makeSound メソッドは、Duck タイプのパラメータのみを受け入れるように規定されているためです。
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 ); //输出:嘎嘎嘎 } }
某些时候,在享受静态语言类型检查带来的安全性的同时,我们亦会感觉被束缚住了手脚。
为了解决这一问题,静态类型的面向对象语言通常被设计为可以向上转型:当给一个类变量赋值时,这个变量的类型既可以使用这个类本身,也可以使用这个类的超类。这就像我们在描述天上的一只麻雀或者一只喜鹊时,通常说“一只麻雀在飞”或者“一只喜鹊在飞”。但如果想忽略它们的具体类型,那么也可以说”一只鸟在飞“。
同理,当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,在实际开发中也许不会如此顺利,这时候可以借助适配器模式来解决问题。
以上就是本文的全部内容,很全面,以生动的举例来帮助大家学习多态,希望大家能够真正的有所收获。