开闭原则指出“软件实体(类、模块、函数等)应该对扩展开放,对修改关闭”。
然而,Joshua Bloch 在他的名著《Effective Java》中给出了以下建议:“设计和记录继承,否则禁止它”,并鼓励程序员使用“final”修饰符来禁止子类化。
我认为这两个原则显然相互矛盾(我错了吗?)。您在编写代码时遵循哪个原则,为什么?您是让您的类保持打开状态,禁止对其中一些类(哪些类?)进行继承,还是尽可能使用 final 修饰符?
开闭原则指出“软件实体(类、模块、函数等)应该对扩展开放,对修改关闭”。
然而,Joshua Bloch 在他的名著《Effective Java》中给出了以下建议:“设计和记录继承,否则禁止它”,并鼓励程序员使用“final”修饰符来禁止子类化。
我认为这两个原则显然相互矛盾(我错了吗?)。您在编写代码时遵循哪个原则,为什么?您是让您的类保持打开状态,禁止对其中一些类(哪些类?)进行继承,还是尽可能使用 final 修饰符?
坦率地说,我认为开放/封闭原则与其说是不合时宜的,不如说是不合时宜的。它似乎来自 80 年代和 90 年代,当时 OO 框架建立在一切都必须从其他东西继承并且一切都应该是可子类化的原则之上。
这在 MFC 和 Java Swing 等时代的 UI 框架中最为典型。在 Swing 中,您有荒谬的继承,其中 (iirc) 按钮扩展复选框(或相反)给其中一个未使用的行为(我认为这是它对复选框的 setDisabled() 调用)。为什么他们有共同的祖先?没有别的原因,好吧,他们有一些共同的方法。
如今,组合比继承更受青睐。Java 默认允许继承,而.Net 采用了(更现代的)默认禁止继承的方法,我认为这更正确(并且更符合 Josh Bloch 的原则)。
DI/IoC 也进一步说明了组合的情况。
Josh Bloch 还指出继承破坏了封装,并给出了一些很好的例子来说明原因。还证明,如果通过委托而不是扩展类来更改 Java 集合的行为,则更加一致。
就我个人而言,这些天来,我在很大程度上认为继承只不过是一个实现细节。
我不认为这两种说法相互矛盾。一个类型可以对扩展开放,对继承仍然关闭。
一种方法是使用依赖注入。一个类型可以在创建时提供这些实例,而不是创建它自己的辅助类型的实例。这允许您更改类型的部分(即打开以进行扩展)而不更改类型本身(即关闭以进行修改)。
在开闭原则(对扩展开放,对修改关闭)您仍然可以使用 final 修饰符。这是一个例子:
public final class ClosedClass {
private IMyExtension myExtension;
public ClosedClass(IMyExtension myExtension)
{
this.myExtension = myExtension;
}
// methods that use the IMyExtension object
}
public interface IMyExtension {
public void doStuff();
}
对类内部的ClosedClass
修改关闭,但对通过另一个类的扩展开放。在这种情况下,它可以是任何实现IMyExtension
接口的东西。这个技巧是依赖注入的一种变体,因为我们正在向封闭类提供另一个,在这种情况下是通过构造函数。由于扩展是一个interface
它不能,final
但它的实现类可以。
在 java 中对类使用 final 来关闭它们类似于sealed
在 C# 中使用。在 .NET 方面也有类似的讨论。
我非常尊重 Joshua Bloch,我认为Effective Java几乎就是Java 圣经。但我认为自动默认private
访问通常是一个错误。我倾向于protected
默认设置,以便至少可以通过扩展类来访问它们。这主要是出于对组件进行单元测试的需要,但我也发现它可以方便地覆盖类的默认行为。当我在自己公司的代码库中工作并最终不得不复制和修改源代码时,我发现这很烦人,因为作者选择“隐藏”所有内容。如果它完全在我的权力范围内,我会游说更改访问权限protected
以避免重复,恕我直言,这要糟糕得多。
还要记住,Bloch 的背景是设计非常 公共的基础 API 库;获得此类代码“正确”的标准必须设置得非常高,因此很可能与您将编写的大多数代码的情况不同。重要的库(例如 JRE 本身)往往更具限制性,以确保该语言不被滥用。查看JRE 中所有已弃用的 API?几乎不可能更改或删除它们。你的代码库可能不是一成不变的,所以如果你最初犯了一个错误,你确实有机会解决问题。
现在我默认使用 final 修饰符,几乎反射性地作为样板的一部分。当您知道给定方法将始终按照您现在正在查看的代码中看到的那样运行时,它使事情变得更容易推理。
当然,有时在某些情况下,类层次结构正是您想要的,那么不使用它会很愚蠢。但是要害怕超过两个级别的层次结构,或者非抽象类被进一步细分的层次结构。一个类应该是抽象的或最终的。
大多数时候,使用组合是要走的路。将所有常见的机器归为一个类,将不同的案例归为不同的类,然后将实例组合成工作整体。
您可以将其称为“依赖注入”、“策略模式”或“访问者模式”或其他任何名称,但归结为使用组合而不是继承来避免重复。
两种说法
软件实体(类、模块、函数等)应该对扩展开放,对修改关闭。
和
设计和记录继承,否则禁止。
彼此并不直接矛盾。只要您为其设计和记录(根据 Bloch 的建议),您就可以遵循开闭原则。
我不认为布洛赫说你应该更喜欢使用 final 修饰符来禁止继承,只是你应该明确地选择在你创建的每个类中允许或禁止继承。他的建议是您应该考虑并自己决定,而不是仅仅接受编译器的默认行为。
我不认为最初提出的开放/封闭原则允许解释最终类可以通过注入依赖项来扩展。
在我的理解中,原则就是不允许对已投入生产的代码进行直接更改,而在仍然允许修改功能的同时实现这一点的方法是使用实现继承。
正如第一个答案所指出的,这有历史根源。几十年前,继承受到青睐,开发人员测试闻所未闻,代码库的重新编译通常需要很长时间。
此外,考虑到在 C++ 中,类的实现细节(特别是私有字段)通常暴露在“.h”头文件中,因此如果程序员需要更改它,所有客户端都需要重新编译。请注意,Java 或 C# 等现代语言并非如此。此外,我认为当时的开发人员不能指望能够执行动态依赖分析的复杂 IDE,从而避免频繁的完全重建。
final
以我自己的经验,我更喜欢做完全相反的事情:“默认情况下,类应该对扩展 () 关闭,但对修改开放”。想一想:今天我们喜欢版本控制(使恢复/比较类的以前版本变得容易)、重构(鼓励我们修改代码以改进设计,或作为引入新功能的前奏)和开发人员等实践testing,它在修改现有代码时提供了一个安全网。