学习JavaScript设计模式(多态)


Posted in Javascript onNovember 25, 2015

多态的实际含义是:同一操作作用于不同的对象上面,可以产生不同的解释和不同的执行结果。换句话说,给不同的对象发送同一个消息的时候,这些对象会根据这个消息分别给出不同的反馈。

从字面上来理解多态不太容易,下面我们来举例说明一下。

主人家里养了两只动物,分别是一只鸭和一只鸡,当主人向它们发出“叫”的命令时,鸭会“嘎嘎嘎”地叫,而鸡会“咯咯咯”地叫。这两只动物都会以自己的方式来发出叫声。它们同样“都是动物,并且可以发出叫声”,但根据主人的指令,它们会各自发出不同的叫声。

其实,其中就蕴含了多态的思想。下面我们通过代码进行具体的介绍。

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

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

Javascript 相关文章推荐
js每次Title显示不同的名言
Sep 25 Javascript
用js模拟JQuery的show与hide动画函数代码
Sep 20 Javascript
原生js实现类似弹窗抖动效果
Apr 02 Javascript
JS实现简单的二元方程计算器功能示例
Jan 03 Javascript
详解微信小程序Page中data数据操作和函数调用
Sep 27 Javascript
手机注册发送验证码倒计时的简单实例
Nov 15 Javascript
JS实现多物体运动的方法详解
Jan 23 Javascript
vue-cli+webpack项目 修改项目名称的方法
Feb 28 Javascript
基于Vue实现拖拽效果
Apr 27 Javascript
使用pm2自动化部署node项目的方法步骤
Jan 28 Javascript
vue的三种图片引入方式代码实例
Nov 19 Javascript
Node.js API详解之 assert模块用法实例分析
May 26 Javascript
创建自己的jquery表格插件
Nov 25 #Javascript
一不小心就做错的JS闭包面试题
Nov 25 #Javascript
javascript Slip.js实现整屏滑动的手机网页
Nov 25 #Javascript
javascript巧用eval函数组装表单输入项为json对象的方法
Nov 25 #Javascript
javascript数据结构之双链表插入排序实例详解
Nov 25 #Javascript
js获取图片宽高的方法
Nov 25 #Javascript
javascript数据结构之二叉搜索树实现方法
Nov 25 #Javascript
You might like
php面向对象中的魔术方法中文说明
2014/03/04 PHP
php实现将任意进制数转换成10进制的方法
2015/04/17 PHP
php实现倒计时效果
2015/12/19 PHP
laravel自定义分页的实现案例offset()和limit()
2019/10/15 PHP
php gethostbyname获取域名ip地址函数详解
2010/01/24 Javascript
浏览器加载、渲染和解析过程黑箱简析
2012/11/29 Javascript
数据结构中的各种排序方法小结(JS实现)
2016/07/23 Javascript
用AngularJS的指令实现tabs切换效果
2016/08/31 Javascript
js css3实现图片拖拽效果
2017/03/04 Javascript
jQuery实现选中行变色效果(实例讲解)
2017/07/06 jQuery
Vue组件通信实践记录(推荐)
2017/08/15 Javascript
vue指令只能输入正数并且只能输入一个小数点的方法
2018/06/08 Javascript
Vue 实现展开折叠效果的示例代码
2018/08/27 Javascript
详解用vue2.x版本+adminLTE开源框架搭建后台应用模版
2019/03/15 Javascript
Vue3 中的数据侦测的实现
2019/10/09 Javascript
python创建只读属性对象的方法(ReadOnlyObject)
2013/02/10 Python
python魔法方法-属性转换和类的表示详解
2016/07/22 Python
在python里协程使用同步锁Lock的实例
2019/02/19 Python
python多线程使用方法实例详解
2019/12/30 Python
.dcm格式文件软件读取及python处理详解
2020/01/16 Python
CSS实现雨滴动画效果的实例代码
2019/10/08 HTML / CSS
使用HTML5 Canvas绘制直线或折线等线条的方法讲解
2016/03/14 HTML / CSS
Mio Skincare中文官网:肌肤和身体护理
2016/10/26 全球购物
西班牙土拨鼠床垫公司,感觉在云端:Marmota
2019/03/18 全球购物
Raffaello Network德国:意大利拉斐尔时尚购物网
2019/05/01 全球购物
学术会议邀请函范文
2014/01/22 职场文书
婚纱店策划方案
2014/05/22 职场文书
啦啦队口号大全
2014/06/16 职场文书
高校群众路线教育实践活动剖析材料
2014/10/10 职场文书
2014年综治维稳工作总结
2014/11/17 职场文书
办公室岗位职责范本
2015/04/11 职场文书
2015年党小组工作总结
2015/05/26 职场文书
关于python3 opencv 图像二值化的问题(cv2.adaptiveThreshold函数)
2022/04/04 Python
win11无线投屏在哪设置? win11无线投屏功能的使用方法
2022/04/08 数码科技
Windows Server 2012配置DNS服务器的方法
2022/04/29 Servers
css3手动实现pc端横向滚动
2022/06/21 HTML / CSS