来自:https://juejin.cn/post/6844904126011146254
目录:
- 看似“多态”的一段代码
- 对象的多态性
- 强制型语言与多态
- TypeScript中使用继承得到的多态效果
- JS与生俱来的多态性
- 多态的实际应用
1.看似“多态”的一段代码
我们听到的比较多的一种说法是:
多态的实际含义是:同一操作作用于不同的对象上,可以产生不同的解释和不同的执行结果。
用例子来说话吧:
假设我家现在养了两只宠物,一只是小猫咪,一只是小狗,现在我对它们发号一个指令让它们叫。
小猫咪需要“喵喵喵~”的叫,而小狗则要“汪汪汪!”。
让它们叫就是同一操作,叫声不同就是不同的执行结果。
如果想要你转换为代码你会如何设计呢?
让我们来理一下,我们需要:
- 一个发出声音的方法makeSound
- 一个猫的类Cat
- 一个狗的类Dog
- 当调用makeSound并且传入的是一只猫则打印出“喵喵喵~”
- 当调用makeSound并且传入的是一只狗则打印出“汪汪汪!”
那我们就可以很容易得出以下这段代码了:
function makeSound(animal) {
if(animal instanceof Cat) {
console.log(‘喵喵喵~‘)
} else if(animal instanceof Dog) {
console.log(‘汪汪汪!‘)
}
}
class Cat{}
class Dog{}
makeSound(new Cat()) // ‘喵喵喵‘
makeSound(new Dog()) // ‘汪汪汪!‘
当然上面这种做法虽然实现了我们想要的“多态性”,makeSound确实是实现了传入不同类型的对象就产生不同效果的功能,但是想想如果现在我家又多养一只小猪呢? 我想让它发出“哼唧唧”的叫声是不是又得去修改makeSound方法呢?
function makeSound(animal) {
if(animal instanceof Cat) {
console.log(‘喵喵喵~‘)
} else if(animal instanceof Dog) {
console.log(‘汪汪汪!‘)
} else if(animal instanceof Pig) {
console.log(‘哼唧唧‘)
}
}
class Cat{}
class Dog{}
class Pig{}
makeSound(new Cat()) // 喵喵喵~
makeSound(new Dog()) // 汪汪汪!
makeSound(new Pig()) // 哼唧唧
当我们每次需要多添加一种动物的时候,都需要去改公共的方法makeSound,并不停的往里面对其条件分支,这显然不是我想要的。因为我们知道修改代码总是危险的,特别是修改这种带有公共性质的方法,程序出错的可能性会增大。并且随着动物种类越来越多,我们的makeSound函数的代码也会越来越多。
2.对象的多态性
其实多态性最根本的作用就是通过把过程化的条件语句转化为对象的多态性,从而消除这些条件分支语句。
它背后的思想通俗点来说就是把“做什么”和“谁去做以及怎么做”分离开。你也可以认为是把“不变的事物”和“可能改变的事物”分离开。
这个例子中既然我们已经明确了‘动物都会发出叫声’(它就是‘不变的的事物’),而动物‘具体怎么叫’是不同的(它就是‘可能改变的事物’),那我们就可以把‘具体怎么叫’这个动作分不到各个类上(封装到各个类上),然后在发出叫声的makeSound函数中调用‘叫’这个动作就可以了。
让我们用多态的思想来改造一下上面的题目:
function makeSound(animal) {
if(animal.sound instanceof Function) { // 判断是否有animal.sound且该属性为函数
animal.sound()
}
}
class Cat {
sound() {
console.log(‘喵喵喵~‘)
}
}
class Dog {
sound() {
console.log(‘汪汪汪!‘)
}
}
class Pig {
sound() {
console.log(‘哼唧唧‘)
}
}
makeSound(new Cat()) // ‘喵喵喵~‘
makeSound(new Dog()) // ‘汪汪汪!‘
makeSound(new Pig()) // ‘哼唧唧‘
现在我们看到的是:调用makeSound方法并传入不同的类,就会有不同的表现形式,并且后续如果我们再需要添加其他的动物,也不需要再去改公共的makeSound方法了,只要这个类中有一个叫sound的方法就可以自动执行了。并且也消除了makeSound中各个if...else。
在这个例子中我们就是把‘不变的部分’动物都会叫(animal.sound())隔离开来,把‘可变的部分’动物具体怎么叫的(sound(){console.log(‘喵喵喵~‘)})各自封装起来。
你还可以怎么理解它呢?
猫这种动物的叫声使他与生俱来的,是我在要叫它发出叫声之前就已经规定好了的,当我在需要它叫的时候他就知道自己该用‘喵喵喵~’的声音叫了。
而不是像最开始那样,虽然你是只猫,但是你却不知道自己要怎样叫,在我要你发出叫声的时候并且告诉你应该要“喵喵喵~”的叫,你才叫。
3.强类型语言与多态
在上面我们一直使用的是JavaScript来进行案例讲解,这会让你觉得多态的实现似乎是一件非常容易的事情。
这个想法的产生其实来源于我们常听到的一句话:JS是一门弱类型(动态类型)语言。
(也就是我们在定义一个变量的时候可以先不指定它是什么类型的,后面赋值的时候再指定)
就像是我要宠物们叫这个案例一样。在定义makeSound方法的时候,我是传递了一个animal进去的,而且我并没有规定这个animal它是个什么类型的东西,比如我并没有规定它就一定是只猫,或者一定是条狗,因为在JS里可以不要我们这么做,它并没有严格的限制。
但是想想如果换成其它的强类型(静态)语言,来个都听过的,比如java。
当我在定义makeSound方法的时候,它需要我指定animal的类型,假设我就指定为猫好了:
public class AnimalSound {
public Void makeSound(Cat animal) { // Cat animal这段代码的意思就是参数animal的类型是一只猫
animal.sound()
}
}
现在当我们调用makeSound方法的时候并且传入了一只猫(new Cat()),是可以正常执行的。
但是如果调用makeSound方法的时候传入的是一条狗呢?程序就不允许我们这么做了,因为它规定了这只动物必须是只猫。
所以可以看出,强类型语言的的类型检查在给带来安全性的同时也让我们在设计某个程序时有种不能大显身手的感觉。
那么在Java这种强类型的语言中可以怎么解决这个问题呢。
额,它主要是通过向上转型,先给你们来一段概念性的东西:
静态类型的面向对象语言通常被设计为可以向上转型:当给一个类变量赋值的时候,这个变量的类型既可以使用这个类本身,也可以使用这个类的超类。
就像是我们在描述宠物叫的时候,通常是说:‘一只小猫在叫’或‘一只小狗在叫’。但是如果忽略它具体的类型,我们可以说是:‘一直动物在叫’。
同样的,这里我们可以设计 一个类型:Animal,当猫Cat和狗Dog的类型都被隐藏在超类型Animal身后的时候,Cat和Dog都能被交换着使用了。
这是让对象表现出多态性的必经之路,而多态性的表现 正是实现众多设计模式的目标。
这样说好像有些枯燥,让我们看看下面TypeScript的例子。
4.TypeScript中使用继承得到多态效果
如果你觉得Java你看不懂也不想看的话,那我就以TypeScript来给你举例。众所周知它是能让我们写‘静态类型的JS’。我用它来实现静态类型的向上转型。
(不过TypeScript在使用上和一些真正的静态类型语言还是有区别的,这里我只是为了方便你理解所以用它来编写案例)
现在我用上面提到的东西,给makeSound方法指定一个类型Animal,它规定了传入的对象必须会叫才行:
interface Animal {
sound(): void
}
function makeSound(animal: Animal) {
animal.sound()
}
makeSound方法中的参数的意思简单理解就是:
- 需要传入一个animal的对象,这个对象必须是Animal类型的
- 而Animal类型(用interface定义的称之为接口)的限制就是必须要有sound方法
(sound(): void表示的是定义一个名为sound的方法且这个方法的返回值为空)
所以现在让我们来设计一下Cat和Dog:
interface Animal {
sound(): void
}
function makeSound(animal: Animal) {
animal.sound()
}
class Cat {
sound(): void {
console.log(‘喵喵喵~‘)
}
}
class Dog {
// sound(): void {
// console.log(‘汪汪汪!‘)
// }
}
makeSound(new Cat())
makeSound(new Dog())
就像上面这段代码,我把Dog的sound方法隐掉了,那么在调用makeSound的时候类型检查就已经错了,它要求我们传入的对象必须要有sound属性。
(扩展一下,当然你也可以在tsconfig.json中修改一下配置:"noImplicitAny": false,设置了这个属性之后,你的function makeSound(animal) {} 的参数animal可以不用必须指定一个类型,不过这样的话就失去了我讲解这个案例的意义了)
interface Animal {
sound(): void
}
function makeSound(animal: Animal) {
animal.sound()
}
class Cat implements Animal {
sound(): void {
console.log(‘喵喵喵~‘)
}
}
class Dog implements Animal {
sound(): void {
console.log(‘汪汪汪!‘)
}
}
makeSound(new Cat())
makeSound(new Dog())
我们看到了一个生疏的API:implements,意思是:实现、执行、落实。
它的作用其实和extends差不多,只不过extends后面接着的是一个类,而implements规定继承的是一个接口(也就是用interface声明的东西)
使用了implements接口继承的Cat和Dog就表示这两个类中必须要有sound方法才行。
5.JS与生俱来的多态性
从上面的几个案例中我们可以看出,多态的思想实际上是要把“做什么”与“谁去做”分离开,那么要实现这一点,最主要的一点就是要先消除类型之间的耦合关系。
这什么耦合关系其实就是我们前面Java案例中的那种,一旦我们指定了makeSound方法的参数animal是一个Cat之后,那animal这个参数就不能传入一只Dog的对象。即:一旦我们指定了方法的参数是某个类型,它就不能再被替换为另一个类型。
而通过TypeScript那个案例我们又可以知道对于Java这种静态类型语言可以通过向上转型来实现多态。
但是对于JS呢,它的变量类型在运行期是可变的,我的animal参数即可以是Cat也可以是Dog,程序并没有要求我指定它的类型,也就是它并不存在这种类型之间的耦合关系,所以我说它的多态性是与生俱来的。
这也可以看出,一个东西能否叫,只取决于他是否有sound方法,而不取决于它是否是某种类型的对象。
例如下面这个案例,我设计了一个Phone类,它并不是一个动物类型的,但是它有sound方法,所以它也能叫:
在JS中并不需要诸如向上转型之类的技术来取得多态的效果
7.多态性的实际应用
如果你没有接触过一些设计模式的话,可能就没有感受到它的重要性。实际上绝大多数设计模式的实现都离不开多态性的思想,例如组合模式、策略模式等等。(关于设计模式这部分的内容霖呆呆也会写个它们的专题系列 ??,不过可能没那么快)
在实际工作上来说,霖呆呆没怎么用过,所以不敢妄下结论说它用的不多。这边在网上是搜寻了一下它的实际应用,十有八九的案例都是和《JavaScript设计模式与开发实践》一书中的百度地图谷歌地图的案例一样。
案例是这样的,其实和我开始列举的命令宠物们叫差不多,你可以看下题目然后自个想想解法。
假设我们现在要编写一个地图应用,有两家可选的地图API提供商供我们接入自己的应用。但是我们不确定实际用会用哪一家,而两家地图的API都提供了一个show方法,负责在页面上展示整个地图。请你利用多态的思想设计一个showMap方法,传入不同的地图供应商进去渲染出不同的地图。
-
高德:GaodeMap
-
搜狗:SogouMap
代码如下:class GaodeMap { show () { console.log(‘高德地图持续为您导航‘) } } class SogouMap { show () { console.log(‘欢迎使用搜狗地图‘) } } function showMap (map) { if (map.show instanceof Function) { map.show() } } showMap(new GaodeMap()) showMap(new SogouMap())