【面向对象】关于继承

博客围绕面向对象编程的继承问题展开。介绍了香蕉猴子丛林、菱形继承等问题,指出类层次不宜过深、避免环。还阐述基类改动对继承类的影响,建议用包含和委托解决。同时反思层次分类,提出用标签分类,强调包含关系在真实世界的应用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1. 香蕉猴子丛林问题

使用一个类,但牵扯到许多类。

Erlang 的创建者 JoeArmstrong 有句名言:
面向对象语言的问题在于,它们依赖于特定的环境。你想要个香蕉,但拿到的却是拿着香蕉的>猩猩,乃至最后你拥有了整片丛林。

解决办法:不要把类层次建得那么深。
但如果继承是重用的关键,那么给继承机制添加的任何限制都会限制重用。
这是建议使用:包含和委托(Contain and Delegate)。

2. 菱形继承问题

在这里插入图片描述

Class PoweredDevice {
}
Class Scanner inherits from PoweredDevice {
  function start() {
  }
}
Class Printer inherits from PoweredDevice {
  function start() {
  }
}
Class Copier inherits from Scanner, Printer {
}

Scanner 和 Printer 类都实现了名为 start 方法。但Copier继承哪个start?

解决办法:不要这样做。如果必须这样建模,使用包含和委托。

Class PoweredDevice {
}
Class Scanner inherits from PoweredDevice {
  function start() {
  }
}
Class Printer inherits from PoweredDevice {
  function start() {
  }
}
Class Copier {
  Scanner scanner
  Printer printer
  function start() {
    printer.start()
  }
}

Copier 类包含一个 Printer 实例和一个 Scanner 实例。然后将 start 函数委托给 Printer 类的实现。要委托给 Scanner 同理。

总结 1 和 2:尽量使用较浅的类层次结构,并保证里面没有环。

3. 关于基类

一个基类如下:

import java.util.ArrayList;

public class Array
{
  private ArrayList<Object> a = new ArrayList<Object>();

  public void add(Object element)
  {
    a.add(element);
  }

  public void addAll(Object elements[])
  {
    for (int i = 0; i < elements.length; ++i)
      a.add(elements[i]); // this line is going to be changed
  }
}

这个类的接口上有两个函数:add() 和 addAll()。add() 函数负责添加一个元素,addAll() 函数会调用 add 函数添加多个元素。 注意加注释的行,后面讲解这个坑。
下面一个继承类:

public class ArrayCount extends Array
{
  private int count = 0;

  @Override
  public void add(Object element)
  {
    super.add(element);
    ++count;
  }

  @Override
  public void addAll(Object elements[])
  {
    super.addAll(elements);
    count += elements.length;
  }
}

ArrayCount类是通用的Array类的特化。两者行为上的唯一区别就是ArrayCount会维护一个count,记录元素的个数。

下面分析一下:
Array的add()给局部的ArrayList添加一个元素。
Array的addAll()针对每个元素调用局部的ArrayList的add方法。
ArrayCount的add()调用父类的add()然后增加count。
ArrayCount的addAll()调用父类的addAll()然后给count增加相当于元素个数的数。
一切都很正常。
现在问题来了,基类中加注释的那行代码现在改成这样:

public void addAll(Object elements[])
  {
    for (int i = 0; i < elements.length; ++i)
      add(elements[i]); // this line was changed
  }

从基类作者的角度来看,这个类实现的功能完全没有变化。而且所有自动化测试也都通过了。
但是基类的作者忘记了继承的类,所以继承类的作者便被错误吵醒了。
现在ArrayCount的addAll()调用父类的addAll(),后者在内部调用add(),而add()被继承类重载了。
因此,每次继承类的add()被调用时,count都会增加,然后在继承类的addAll()被调用时再次增加。
count被增加了两次。
既然会发生这种现象,那么继承类的作者必须清楚基类是怎样实现的。而且,基类的每个改动必须要通知所有继承类的作者,因为这些改动可能会以不可预知的方式破坏继承类。

解决方法
这个问题还得要包含和委托来解决。
使用包含和委托,可以从白盒编程转到黑盒编程。白盒编程的意思是说,写继承类时必须要了解基类的实现。
而黑盒编程可以完全无视基类的实现,因为不可能通过重载函数的方式向基类注入代码。只需要关注接口即可。
这种趋势太讨厌了……
继承本应带来最好用的重用。
在面向对象语言中实现包含和委托并不容易。它们是为了继承方便而设计的。
如果你和我一样,你就会开始反思这个继承了。但更重要的是,这些问题应当引起你对于通过层次结构进行分类的反思。

4. 层次结构的问题

每到一个新公司时,我都要为在哪儿保存公司文档(即员工手册)而纠结。
是应该建一个Documents文件夹,然后在里面建个Company呢?
还是应该建个Company文件夹,然后在里面建个Documents呢?
两者都可以。但哪个是正确的?哪个更好?
层次分类的思想是因为基类(父类)更通用,继承类(子类)更专用。沿着继承链越往下走,概念就越专用。
但如果父节点和子节点能随意交换位置,那么显然这种模型是有问题的。

真正的问题出在层次分类是错误的。
那层次分类应该用在哪里?
包含关系。
真实世界里有很多包含关系(或者叫做独占关系)的层次结构。
但你找不到层次分类。仔细想一下,面向对象范式是根据充满了各种对象的真实世界建立的。但它用错了模型——层次分类在真实世界中没有类比。
真实世界里到处都是层次包含关系。层次包含关系的一个非常好的例子就是你的袜子。袜子放在装袜子的抽屉里,然后抽屉包含在衣柜里,衣柜包含在卧室里,卧室包含在房子里,等等。
硬盘上的目录也是层次包含关系的另一个例子——它们包含文件。
那我们该怎样分类呢?
仔细想一下公司文档,就会发现其实放在哪儿都无所谓。我可以放在Documents目录下或者放在Stuff目录下也可以。
我选择的分类法是标签。我给它加上不同的标签。

参考:

https://mp.weixin.qq.com/s?__biz=MzA4NDY3ODAwNA==&mid=2651234676&idx=1&sn=c5598c8577dd58e5f41caaf4fdf322b0&chksm=8411856fb3660c790332083d27e232b7da7556d7d0c1fc01e2420fbbd874db99cd8b8fd93eb3&mpshare=1&scene=23&srcid=&sharer_sharetime=1585705636549&sharer_shareid=1f90a8080b8d75a3636ab2ea2c26028b#rd

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

万俟淋曦

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值