你如何设计一个继承类?

时间:2009-01-17 19:33:11

标签: language-agnostic inheritance

我听说它说“设计继承”是“困难的”,但我从来没有发现过这种情况。任何人(以及任何人,我的意思是Jon Skeet)都可以解释为什么这可能是困难的,陷阱/障碍/问题是什么,为什么凡人的程序员不应该尝试它而只是让他们的课程密封以保护无辜者?

好吧,我嘲笑后者 - 但我很想知道是否有人(包括Jon)确实有“设计继承”的困难。我真的从来没有把它视为一个问题,但也许我忽略了一些我认为理所当然的东西 - 或者在没有意识到的情况下搞砸了一些东西!

编辑:感谢迄今为止所有出色的答案。我相信共识是​​,对于典型的应用程序类(WinForm子类,一次性实用程序类等),不需要考虑任何类型的重用,更不用说通过继承重用了,而对于库类,考虑重用是至关重要的。通过设计中的继承。

我并没有真的想到一个WinForm类来实现一个GUI对话框,作为一个有人可能会重用的类 - 我有点认为它是一个一次性的对象。但从技术上讲,它是一个类,有人可能会继承它 - 但它不太可能。

我所做的很多大规模开发都是基础库和框架的类库,因此通过继承进行重用设计是至关重要的 - 我从来没有认为它是“困难的”,只是。 ; - )

但我也从未考虑过与WinForms等人的常见应用程序任务的“一次性”类别相比。

当然,欢迎设计继承的更多提示和陷阱;我也试着投入一些。

5 个答案:

答案 0 :(得分:19)

我只是回答你应该看一下the problems I had when subclassing java.util.Properties,而不是过多地重复它。我倾向于尽可能少地避免设计继承的许多问题。以下是对这些问题的一些看法:

  • 正确实现相等性是一种痛苦(或可能是不可能的),除非您将其限制为“两个对象必须具有完全相同的类型”。困难来自a.Equals(b)暗示b.Equals(a)的对称要求。如果ab分别是“10x10平方”和“a 红色 10x10平方”,那么a可能会认为b是等于它 - 在许多情况下,可能是你真正想要测试的所有东西。但是b知道它有颜色而a没有......

  • 每当您在实施中调用虚拟方法时,您必须记录该关系,并且永远不会更改它。从类派生的人也需要读取该文档 - 否则他们可能反过来实现调用,很快导致X调用Y的堆栈溢出,调用Y调用Y.这是我的主要问题 - 在许多您需要记录实施的情况,这会导致缺乏灵活性。如果你从来没有从另一个虚拟方法中调用一个虚拟方法,那么它会显着减轻,但你仍然必须记录对虚拟方法的任何其他调用,即使是非虚拟方法,也永远不会改变实现。< / p>

  • 即使没有一些未知代码成为执行时类的一部分,线程安全也很难实现。您不仅需要记录类的线程模型,还可能需要将锁(等)暴露给派生类,以便它们可以以相同的方式保持线程安全。

  • 在保持原始基类合同的同时考虑哪种特化是有效的。在什么方面可以覆盖方法,以便调用者不需要知道专业化,只是它有效? java.util.Properties是一个很糟糕的特殊化的好例子 - 调用者不能只把它当作普通的Hashtable,因为它只应该包含键和值的字符串。

  • 如果一个类型是不可变的,它不应该允许继承 - 因为子类很容易变成可变的。然后可能会出现奇怪的情况。

  • 你应该实现某种克隆能力吗?如果不这样做,可能会使子类更难以正确克隆。有时成员克隆就足够了,但有时候它可能没有意义。

  • 如果您不提供任何虚拟成员,您可能会相当安全 - 但此时任何子类都提供额外的不同功能,而不是专门化现有功能。那不是必然是坏事,但它不像继承的最初目的。

对于应用程序构建者而言,其中许多问题远远少于类库设计者:如果您知道您和您的同事将成为唯一一个从您的类型中获得的人,那么您可以获得很多较少的前期设计 - 如果您以后需要,可以修复子类。

顺便提一下,这些只是袖手旁观。如果我想的话,我可能会想出更多。 Josh Bloch在Effective Java 2中说得非常好,顺便说一句。

答案 1 :(得分:5)

我认为主要观点是Jon的回答中的倒数第二段:大多数人都认为应用程序设计中的“继承设计”,即他们只是希望它能够工作,如果不能,它可以修复。

但是将一个可继承的类设计为供其他人使用的API是一个完全不同的野兽 - 因为那时被保护的字段和大量的实现细节隐含地成为API合同的一部分,人们使用API​​必须了解,如果不使用API​​破坏代码,则无法更改。如果你在设计或实现中犯了错误,很可能无法在不破坏依赖它的代码的情况下修复它。

答案 2 :(得分:3)

我认为我没有为继承设计一个类。我只是编写尽可能少的代码,然后当复制和粘贴时间创建另一个类时,我将一个方法碰到一个超类(它比组合更有意义)。所以我让“不要重复自己”(DRY)原则决定我的班级设计。

至于消费者,他们应该采取合理的态度。我尽量不决定任何人如何使用我的课程,尽管我确实试着记录我打算如何使用它们。

答案 3 :(得分:2)

另一个可能的问题:当从基类中的构造函数调用'virtual'方法时,子类会覆盖此方法,子类可能会使用单位化数据。

代码更好:

class Base {
  Base() {
    method();
  }

  void method() {
    // subclass may override this method
  } 
}

class Sub extends Base {
  private Data data;

  Sub(Data value) {
    super();
    this.data = value;
  }

  @Override
  void method() {
    // prints null (!), when called from Base's constructor
    System.out.println(this.data);  
  }
}

这是因为基类的构造函数必须始终在子类的构造函数之前完成。

摘要:不要从构造函数中调用可覆盖的方法

答案 4 :(得分:1)

有时在创建基类时,我不确定是否应该将某些成员暴露给子类,例如我用来同步的对象。我通常最终将它们全部设为私有,并在需要时将其更改为受保护。