【问题标题】:How do you design a class for inheritance?你如何设计一个继承类?
【发布时间】:2009-01-17 19:33:11
【问题描述】:

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

好吧,我对后者开玩笑-但我很想知道是否有人(包括 Jon)真的在“为继承而设计”时遇到困难。我真的从来没有认为这是一个问题,但也许我忽略了一些我认为理所当然的事情 - 或者在没有意识到的情况下搞砸了一些事情!

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

我并没有真正考虑过,比如说,一个 WinForm 类来实现一个 GUI 对话框,作为一个有人可以重用的类 - 我有点认为它是一个一次性的对象。但从技术上讲,它是一个类,有人可能会从它继承——但这不太可能。

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

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

当然,欢迎更多关于继承设计的技巧和陷阱;我也试试看。

【问题讨论】:

    标签: language-agnostic inheritance


    【解决方案1】:

    我认为重点在 Jon 回复的倒数第二段:大多数人从应用程序设计的角度考虑“为继承而设计”,即他们只想让它工作,如果它不工作,可以修复。

    但是将可继承类设计为 API 供其他人使用是完全不同的事情 - 因为受保护的字段和大量实现细节隐含地成为 API 合同的一部分,使用该 API 的人必须这样做理解,并且在不使用 API 破坏代码的情况下无法更改。如果您在设计或实现中犯了错误,您可能无法在不破坏依赖它的代码的情况下修复它。

    【讨论】:

    • +1 是的,但是很多时候,基类中的设计错误和疏忽可以很容易地在子类中得到纠正,除非设计者自大到认为他所有的时间都是完整和正确的,即基类是密封的或者主要的方法是非虚的!
    • 问题是基类的这种“修复”对于基类的更改往往非常脆弱。
    • @Michael Borgwardt]:同意,但这比使此类修复变得不可能要好得多,非虚拟方法和密封类往往会这样做!
    【解决方案2】:

    我认为我从来没有为继承设计过一个类。我只是编写尽可能少的代码,然后当需要复制和粘贴以创建另一个类时,我将那个方法撞到一个超类中(它比组合更有意义)。所以我让不要重复自己 (DRY) 原则来指导我的课程设计。

    至于消费者,他们要谨慎行事。我尽量不规定任何人如何使用我的课程,尽管我会尝试记录我打算如何使用它们。

    【讨论】:

    • 所以对你来说继承是重构的副产品,不是预先计划好的;一个有趣的观点
    • 这也是我所做的...... Resharper 万岁...... :) Steve,如果你还没有投资 ReSharper,你应该......这是一个很棒的工具。
    【解决方案3】:

    与其重复太多,我会简单地回答你应该看看the problems I had when subclassing java.util.Properties。我倾向于通过尽可能少地这样做来避免为继承设计的许多问题。不过,这里有一些关于问题的想法:

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

    • 任何时候在实现中调用虚拟方法时,都必须记录这种关系,并且永远不要改变它。从类派生的人也需要阅读该文档 - 否则他们可能会以相反的方式实现调用,迅速导致 X 调用 Y 的堆栈溢出,它调用 X 调用 Y。这是我的主要问题 - 在许多在这种情况下,您必须记录您的实施,这会导致缺乏灵活性。如果您从不从另一个虚拟方法调用一个虚拟方法,这种情况会得到显着缓解,但是您仍然必须记录对虚拟方法的任何其他调用,即使是来自非虚拟方法的调用,并且永远不要在这个意义上更改实现。

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

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

    • 如果一个类型是不可变的,它不应该允许继承——因为子类很容易是可变的。那么奇怪的事情就会比比皆是。

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

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

    其中许多对于应用程序构建者来说比类库设计者的问题要小得多:如果您知道您和您的同事将是唯一从您的类型派生的人,那么您可以摆脱很多减少前期设计 - 如果您以后需要,您可以修复子类。

    顺便说一句,这些只是现成的要点。如果我思考的时间足够长,我可能会想出更多。 Josh Bloch 在 Effective Java 2 中说得很好,顺便说一句。

    【讨论】:

    • 一个被称为多态的。在大多数情况下,这与“可以被覆盖”相同 - 即在 Java 术语中,非最终的。
    • 所有对象方法在 Java 中都是虚拟的。这意味着成员函数调用的目标始终由被引用的对象决定,而不是由调用处的引用类型决定。在 C++ 或 C# 中并非总是如此
    • 在 java 中,只有非私有的非最终方法是“虚拟的”。
    • 非私有、非最终、非静态方法:)
    • 我并没有真正获得最终豁免。如果您有继承 A,B,C,D 并且方法 foo 在 B 中定义并在 C 中被覆盖并在 D 中成为最终方法,并且您有 D x = new B(); x.foo();它不是调用 B 的 foo() 吗?如果是这样,那么 foo 根据任何合理的定义都是虚拟的。如果不是,那么 Java 就疯了。
    【解决方案4】:

    还有一个可能的问题:当你从基类的构造函数中调用'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);  
      }
    }
    

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

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

    【讨论】:

    • +1 但我会加强你的总结:不要从构造函数调用任何方法。如果你真的需要,可以为这类事情提供一个明确的 Initialize 方法。
    • @Steven:这使得制作不可变类型来做任何不平凡的事情变得非常非常困难。 (是的,你可以有一个静态方法调用构造函数然后初始化,并使类型私有可变,但我更喜欢“完全”不可变的只读字段等)
    • @[Jon Skeet]:你必须@我的全名,否则我在回复标签中看不到它。我绝对是 ROFLMAO,你还不知道这一点 ;-) 而且我认为不可变类型是一个罕见的例外
    • @[Steven A. Lowe]:我尽可能将大多数数据类设计为不可变的。他们不是一个罕见的例外。
    【解决方案5】:

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

    【讨论】:

    • 同步对象应该受到保护,以便子类也可以(并且应该!)使用它们
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2023-01-12
    • 1970-01-01
    • 2014-11-22
    • 1970-01-01
    • 1970-01-01
    • 2020-09-10
    相关资源
    最近更新 更多