本文主要包含以下知识点:
- 多态介绍
- Java 中的多态
- JavaScript 中的多态
- 多态在面向对象程序设计中的作用
多态介绍
“多态”一词源于希腊文 polymorphism,拆开来看是 poly(复数)+ morph(形态)+ ism,从字面上我们可以理解为复数形态。
多态的实际含义是:同一操作作用于不同的对象上面,可以产生不同的解释和不同的执行结果。换句话说,给不同的对象发送同一个消息的时候,这些对象会根据这个消息分别给出不同的反馈。
从字面上来理解多态不太容易,下面我们来举例说明一下。
主人家里养了两只动物,分别是一只鸭和一只鸡,当主人向它们发出“叫”的命令时,鸭会“嘎嘎嘎”地叫,而鸡会“咯咯咯”地叫。这两只动物都会以自己的方式来发出叫声。它们同样“都是动物,并且可以发出叫声”,但根据主人的指令,它们会各自发出不同的叫声。
其实,其中就蕴含了多态的思想。下面我们通过代码进行具体的介绍。
Java 中的多态
在介绍 JavaScript 中的多态之前,我们先来看一下类似于 Java 这样的静态语言中的多态。
有下面这么一段代码:
// 鸭子类
class Duck {
public void makeSound() {
System.out.println("嘎嘎嘎");
}
}
// 鸡类
class Chicken {
public void makeSound() {
System.out.println("咯咯咯");
}
}
// 动物类
// 接收一只动物(这里特指鸭子),调用该动物的 makeSound 方法
class AnimalSound {
public void makeSound(Duck duck) {
duck.makeSound();
}
}
// 主函数类
public class Test {
public static void main(String args[]) {
AnimalSound animalSound = new AnimalSound();
Duck duck = new Duck();
animalSound.makeSound(duck); // 输出:嘎嘎嘎
}
}
在上面的代码中,我们创建了一个鸭子类和一个鸡类,分别都拥有 makeSound 方法。创建了一个 AnimalSound 类,接收一个 Duck 类型的对象,然后调用该对象的 makeSound 方法。
至此,我们已经顺利地让鸭子可以发出叫声,但如果现在想让鸡也叫唤起来,我们发现这是一件不可能实现的事情。因为 AnimalSound 类的 makeSound 方法,被我们规定为只能接受 Duck 类型的参数:
// 主函数类
public class Test {
public static void main(String args[]) {
AnimalSound animalSound = new AnimalSound();
Duck duck = new Duck();
animalSound.makeSound(duck); // 输出:嘎嘎嘎
// 添加如下的代码,新实例化一只鸡,传入到 animalSound 对象的 makeSound 方法中
Chicken chicken = new Chicken();
animalSound.makeSound(chicken); // 报错,只能接受 Duck 类型的参数
}
}
为了解决这一问题,静态类型的面向对象语言通常被设计为可以向上转型:当给一个类变量赋值时,这个变量的类型既可以使用这个类本身,也可以使用这个类的超类。
这就像我们在描述天上的一只麻雀或者一只喜鹊时,通常说“一只麻雀在飞”或者“一只喜鹊在飞”。但如果想忽略它们的具体类型,那么也可以说“一只鸟在飞”。
同理,当 Duck 对象和 Chicken 对象的类型都被隐藏在超类型 Animal 身后,Duck 对象和 Chicken 对象就能被交换使用,这是让对象表现出多态性的必经之路,而多态性的表现正是实现众多设计模式的目标。
如果要在 Java 中实现多态,那么我们首先需要创建一个 Animal 抽象类,再分别让 Duck 和 Chicken 都继承自 Animal 抽象类,代码如下:
// 抽象类
abstract class Animal {
abstract void makeSound(); // 抽象方法
}
// 鸡类继承 Animal 类
class Chicken extends Animal {
public void makeSound() {
System.out.println("咯咯咯");
}
}
// 鸭子类继承 Animal 类
class Duck extends Animal {
public void makeSound() {
System.out.println("嘎嘎嘎");
}
}
class AnimalSound {
// 接受 Animal 类型的参数
public void makeSound(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); // 咯咯咯
}
}
JavaScript 中的多态
由于 JavaScript 是一门动态语言,其变量类型在运行期是可变的。所以一个 JavaScript 对象,既可以表示 Duck 类型的对象,又可以表示 Chicken 类型的对象,不需要提前做类型的检查,甚至我传入一个数组都可以。
所以这也就意味着 JavaScript 对象的多态性是与生俱来的,不需要像 Java 一样向上转型。
下面是一段 JavaScript 中多态的实现:
const makeSound = function (animal) {
animal.sound();
};
const Duck = function () { }
Duck.prototype.sound = function () {
console.log('嘎嘎嘎');
};
const Chicken = function () { }
Chicken.prototype.sound = function () {
console.log('咯咯咯');
};
makeSound(new Duck()); // 嘎嘎嘎
makeSound(new Chicken()); // 咯咯咯
现在我们向鸭和鸡都发出“叫唤”的消息,它们接到消息后分别作出了不同的反应。如果有一天动物世界里又增加了一只狗,这时候只要简单地追加一些代码就可以了,不用改动 makeSound 函数,如下所示:
const Dog = function () { }
Dog.prototype.sound = function () {
console.log('汪汪汪');
};
makeSound(new Dog()); // 汪汪汪
由此可见,某一种动物能否发出叫声,只取决于它有没有 makeSound 方法,而不取决于它是否是某种类型的对象,这里不存在任何程度上的“类型耦合”。
这也是我们从上一节的鸭子类型中所领悟的道理,在 JavaScript 中,并不需要诸如向上转型之类的技术来取得多态的效果。
多态背后的思想是将“做什么”和“谁去做以及怎样去做”分离开来,也就是将“不变的事物”与“可能改变的事物”分离开来。
在上面的示例中,动物都会叫,这是不变的,但是不同类型的动物具体怎么叫是可变的。把不变的部分隔离出来,把可变的部分封装起来,这给予了我们扩展程序的能力,程序看起来是可生长的,也是符合开放封闭原则的,相对于修改代码来说,仅仅增加代码就能完成同样的功能,这显然优雅和安全得多。
多态在面向对象程序设计中的作用
有许多人认为,多态是面向对象编程语言中最重要的技术。但我们目前还很难看出这一点,毕竟大部分人都不关心鸡是怎么叫的,也不想知道鸭是怎么叫的。让鸡和鸭在同一个消息之下发出不同的叫声,这跟程序员有什么关系呢?
Martin Fowler 在《重构:改善既有代码的设计》里写到:
多态的最根本好处在于,你不必再向对象询问“你是什么类型”而后根据得到的答案调用对象的某个行为。你只管调用该行为就是了,其他的一切多态机制都会为你安排妥当。
换句话说,多态最根本的作用就是通过把过程化的条件分支语句转化为对象的多态性,从而消除这些条件分支语句。
Martin Fowler 的话可以用下面这个例子很好地诠释:
在电影的拍摄现场,当导演喊出“action”时,主角开始背台词,照明师负责打灯光,后面的群众演员假装中枪倒地,道具师往镜头里撒上雪花。在得到同一个消息时,每个对象都知道自己应该做什么。
如果不利用对象的多态性,而是用面向过程的方式来编写这一段代码,那么相当于在电影开始拍摄之后,导演每次都要走到每个人的面前,确认它们的职业分工(类型),然后告诉他们要做什么。如果映射到程序中,那么程序中将充斥着条件分支语句。
利用对象的多态性,导演在发布消息时,就不必考虑各个对象接到消息后应该做什么。对象应该做什么并不是临时决定的,而是已经事先约定和排练完毕的。
每个对象应该做什么,已经成为了该对象的一个方法,被安装在对象的内部,每个对象负责它们自己的行为。所以这些对象可以根据同一个消息,有条不紊地分别进行各自的工作。
将行为分布在各个对象中,并让这些对象各自负责自己的行为,这正是面向对象设计的优点。
在面向对象设计中,多态这一特点是重中之重,绝大部分设计模式的实现都离不开多态性的思想。但是,正如我们在“设计模式概述”中所说的一样,不同的语言,在设计模式上面的实现也略有不同。对于某些语言来讲,部分设计模式天生就有。
例如,在 JavaScript 这种将函数作为一等对象的语言中,函数本身也是对象,函数用来封装行为并且能够四处传递。
当我们对一些函数发出“调用”的消息时,这些函数会返回不同的执行结果,这也可以看作是一种“多态性”的体现,同时也是很多设计模式在 JavaScript 中可以用高阶函数来代替实现的原因。
-EOF-