关于继承

  1. 继承是用来表示类之间的 is-a 关系。比如猫是一种哺乳动物。从继承关系上来讲,继承可以分为两种模式,单继承多继承。单继承表示一个子类只继承一个父类,多继承表示一个子类可以继承多个父类,比如猫既是哺乳动物,又是爬行动物。
  2. 通过继承还可以实现多态特性。
  3. 继承最大的一个好处就是代码复用。比如两个类有一些相同的属性和方法,我们就可以将这些相同的部分,抽取到父类中,让两个子类继承父类。

    多用组合少用继承?

    继承最大的问题就是:

  4. 当继承的层次过深时,会导致代码的可读性变差。因为我们要搞清楚某个类具有哪些方法、属性,必须阅读父类的代码、父类的父类的代码……一直追溯到最顶层父类的代码。

  5. 当继承的层次过深时,会导致代码的可维护性变差。子类的实现依赖父类的实现,两者高度耦合,一旦父类代码修改,就会影响所有子类的逻辑。
  6. 这也破坏了类的封装特性,需要将父类的实现细节暴露给了子类。继承这种功能复用方式通常被称为“白箱复用”,“白箱”是相对可见性而言的, 在继承方式中,超类的内部细节是对子类可见的,因此继承常常被认为破坏了封装性。

我们将“鸟类”这样一个抽象的事物概念,定义为一个抽象类 AbstractBird。

但是有些鸟会飞比如麻雀,鸽子,有些鸟不会飞,比如鸵鸟,企鹅,因此我们把AbstractBird 类派生出两个更加细分的抽象类:会飞的鸟类 AbstractFlyableBird 和不会飞的鸟类 AbstractUnFlyableBird。让麻雀、鸽子这些会飞的鸟都继承 AbstractFlyableBird,让鸵鸟、企鹅这些不会飞的鸟,都继承 AbstractUnFlyableBird 类。

现在类的层次有三层,在刚刚这个场景中,我们只关注“鸟会不会飞”,但如果我们还关注“会不会叫”,“会不会下蛋”,那类的层次会越来越复杂

我们可以利用组合(composition)、接口(interface)、委托(delegation)三个技术手段,一块儿来解决刚刚继承存在的问题。

继承的两个作用,

  1. 对于实现多态,我们可以用接口代替。
  2. 对于代码复用,我们可以用组合+委托代替。

使用接口实现多态
针对“会飞”这样一个行为特性,我们可以定义一个 Flyable 接口,只让会飞的鸟去实现这个接口。对于会叫、会下蛋这些行为特性,我们可以类似地定义 Tweetable 接口、EggLayable 接口。具体代码如下:

  1. interface Flyable {
  2. fly():void;
  3. }
  4. interface Tweetable {
  5. tweet():void;
  6. }
  7. interface EggLayable {
  8. layEgg():void;
  9. }
  10. class Ostrich implements Tweetable, EggLayable {//鸵鸟
  11. //... 省略其他属性和方法...
  12. tweet(){
  13. console.log('我会叫')
  14. }
  15. layEgg(){
  16. console.log('我会下蛋')
  17. }
  18. }
  19. class Sparrow implements Tweetable, EggLayable {//麻雀
  20. //... 省略其他属性和方法...
  21. fly(){
  22. console.log('我会飞')
  23. }
  24. tweet(){
  25. console.log('我会叫')
  26. }
  27. layEgg(){
  28. console.log('我会下蛋')
  29. }
  30. }

使用组合+委托实现代码复用
接口只声明方法,不定义实现。也就是说,每个会下蛋的鸟都要实现一遍 layEgg() 方法,并且实现逻辑是一样的,这就会导致代码重复的问题。那这个问题又该如何解决呢?

我们可以针对三个接口再定义三个实现类,它们分别是:实现了 fly() 方法的 FlyAbility 类、实现了 tweet() 方法的 TweetAbility 类、实现了 layEgg() 方法的 EggLayAbility 类。然后,通过组合和委托技术来消除代码重复。具体的代码实现如下所示:

  1. interface Flyable {
  2. fly(): void;
  3. }
  4. interface Tweetable {
  5. tweet(): void;
  6. }
  7. interface EggLayable {
  8. layEgg(): void;
  9. }
  10. class FlyAbility implements Flyable {
  11. fly() {}
  12. }
  13. class TweetAbility implements Tweetable {
  14. tweet() {}
  15. }
  16. class EggLayAbility implements EggLayable {
  17. layEgg() {}
  18. }
  19. class Ostrich implements Tweetable, EggLayable {
  20. //鸵鸟
  21. private tweetAbility: TweetAbility = new TweetAbility(); //组合
  22. private eggLayAbility: EggLayAbility = new EggLayAbility(); //组合
  23. //... 省略其他属性和方法...
  24. tweet(): void {
  25. this.tweetAbility.tweet(); // 委托
  26. }
  27. layEgg(): void {
  28. this.eggLayAbility.layEgg(); // 委托
  29. }
  30. }

如何判断该用组合还是继承?

如果类之间的继承结构稳定(不会轻易改变),继承层次比较浅(比如,最多有两层继承关系),继承关系不复杂,我们就可以大胆地使用继承。反之,继承层次很深,继承关系复杂,我们就尽量使用组合来替代继承。

参考文献:
https://time.geekbang.org/column/article/161114
https://time.geekbang.org/column/article/169593