关于继承
- 继承是用来表示类之间的 is-a 关系。比如猫是一种哺乳动物。从继承关系上来讲,继承可以分为两种模式,单继承和多继承。单继承表示一个子类只继承一个父类,多继承表示一个子类可以继承多个父类,比如猫既是哺乳动物,又是爬行动物。
- 通过继承还可以实现多态特性。
继承最大的一个好处就是代码复用。比如两个类有一些相同的属性和方法,我们就可以将这些相同的部分,抽取到父类中,让两个子类继承父类。
多用组合少用继承?
继承最大的问题就是:
当继承的层次过深时,会导致代码的可读性变差。因为我们要搞清楚某个类具有哪些方法、属性,必须阅读父类的代码、父类的父类的代码……一直追溯到最顶层父类的代码。
- 当继承的层次过深时,会导致代码的可维护性变差。子类的实现依赖父类的实现,两者高度耦合,一旦父类代码修改,就会影响所有子类的逻辑。
- 这也破坏了类的封装特性,需要将父类的实现细节暴露给了子类。继承这种功能复用方式通常被称为“白箱复用”,“白箱”是相对可见性而言的, 在继承方式中,超类的内部细节是对子类可见的,因此继承常常被认为破坏了封装性。
我们将“鸟类”这样一个抽象的事物概念,定义为一个抽象类 AbstractBird。
但是有些鸟会飞比如麻雀,鸽子,有些鸟不会飞,比如鸵鸟,企鹅,因此我们把AbstractBird 类派生出两个更加细分的抽象类:会飞的鸟类 AbstractFlyableBird 和不会飞的鸟类 AbstractUnFlyableBird。让麻雀、鸽子这些会飞的鸟都继承 AbstractFlyableBird,让鸵鸟、企鹅这些不会飞的鸟,都继承 AbstractUnFlyableBird 类。
现在类的层次有三层,在刚刚这个场景中,我们只关注“鸟会不会飞”,但如果我们还关注“会不会叫”,“会不会下蛋”,那类的层次会越来越复杂。
我们可以利用组合(composition)、接口(interface)、委托(delegation)三个技术手段,一块儿来解决刚刚继承存在的问题。
继承的两个作用,
- 对于实现多态,我们可以用接口代替。
- 对于代码复用,我们可以用组合+委托代替。
使用接口实现多态
针对“会飞”这样一个行为特性,我们可以定义一个 Flyable 接口,只让会飞的鸟去实现这个接口。对于会叫、会下蛋这些行为特性,我们可以类似地定义 Tweetable 接口、EggLayable 接口。具体代码如下:
interface Flyable {
fly():void;
}
interface Tweetable {
tweet():void;
}
interface EggLayable {
layEgg():void;
}
class Ostrich implements Tweetable, EggLayable {//鸵鸟
//... 省略其他属性和方法...
tweet(){
console.log('我会叫')
}
layEgg(){
console.log('我会下蛋')
}
}
class Sparrow implements Tweetable, EggLayable {//麻雀
//... 省略其他属性和方法...
fly(){
console.log('我会飞')
}
tweet(){
console.log('我会叫')
}
layEgg(){
console.log('我会下蛋')
}
}
使用组合+委托实现代码复用
接口只声明方法,不定义实现。也就是说,每个会下蛋的鸟都要实现一遍 layEgg() 方法,并且实现逻辑是一样的,这就会导致代码重复的问题。那这个问题又该如何解决呢?
我们可以针对三个接口再定义三个实现类,它们分别是:实现了 fly() 方法的 FlyAbility 类、实现了 tweet() 方法的 TweetAbility 类、实现了 layEgg() 方法的 EggLayAbility 类。然后,通过组合和委托技术来消除代码重复。具体的代码实现如下所示:
interface Flyable {
fly(): void;
}
interface Tweetable {
tweet(): void;
}
interface EggLayable {
layEgg(): void;
}
class FlyAbility implements Flyable {
fly() {}
}
class TweetAbility implements Tweetable {
tweet() {}
}
class EggLayAbility implements EggLayable {
layEgg() {}
}
class Ostrich implements Tweetable, EggLayable {
//鸵鸟
private tweetAbility: TweetAbility = new TweetAbility(); //组合
private eggLayAbility: EggLayAbility = new EggLayAbility(); //组合
//... 省略其他属性和方法...
tweet(): void {
this.tweetAbility.tweet(); // 委托
}
layEgg(): void {
this.eggLayAbility.layEgg(); // 委托
}
}
如何判断该用组合还是继承?
如果类之间的继承结构稳定(不会轻易改变),继承层次比较浅(比如,最多有两层继承关系),继承关系不复杂,我们就可以大胆地使用继承。反之,继承层次很深,继承关系复杂,我们就尽量使用组合来替代继承。
参考文献:
https://time.geekbang.org/column/article/161114
https://time.geekbang.org/column/article/169593