为什么一个班级除了“抽象”或“最终/密封”之外,什么都不应该?


29

经过10年以上的Java / C#编程,我发现自己创建了以下任一文件:

  • 抽象类:合同,并非原样实例化。
  • 期末/密封班:实现不打算用作其他任何东西的基类。

我想不出任何简单的“类”(即既不是抽象的也不是最终的/密封的)是“明智的编程”的情况。

为什么一个班级应该不是“抽象”或“最终/密封”之外的任何东西?

编辑

这篇很棒的文章比我能更好地解释我的担忧。


21
因为它叫做Open/Closed principle,而不是Closed Principle.
StuperUser 2012年

2
您是专业从事哪种类型的事情?这很可能会影响您对此事的看法。

好吧,我知道有些平台开发人员会密封所有内容,因为他们想最小化继承接口。
K..

4
@StuperUser:这不是原因,这是陈词滥调。OP并没有问陈词滥调,而是在问为什么。如果没有任何道理在背后,那么就没有理由关注它。
Michael Shaw

1
我想知道通过将Window类更改为Sealed会破坏多少UI框架。
Reactgular 2012年

Answers:


46

具有讽刺意味的是,我发现相反的情况:使用抽象类是一个例外,而不是规则,并且我不喜欢最终/密封类。

接口是一种更典型的按合同设计的机制,因为您没有指定任何内部结构–您不必担心它们。它允许该合同的每个实施都是独立的。这是许多领域的关键。例如,如果您正在构建ORM,则可以以统一的方式将查询传递给数据库非常重要,但是实现可能会大不相同。如果为此目的使用抽象类,则最终会在可能适用于所有实现的组件中进行硬接线。

对于最终/密封类,我能看到的唯一借口是允许覆盖实际上是危险的时候-可能是加密算法或其他东西。除此之外,您永远都不知道出于本地原因何时希望扩展功能。封闭类会限制您在大多数情况下都不存在的收益选择。编写类的方式要灵活得多,以便以后可以扩展它们。

通过使用密封类的第三方组件,我的后一种观点得到了巩固,从而防止了某些使生活变得更加轻松的集成。


17
+1为最后一段。我经常发现第三方库(甚至是标准库类!)中的太多封装是导致痛苦的原因,而不是太少的封装。
梅森惠勒2012年

5
密封类的通常论点是,您无法预测客户端可能会覆盖您的类的许多不同方式,因此您不能对其行为做出任何保证。参见blogs.msdn.com/b/ericlippert/archive/2004/01/22/…–
罗伯特·哈维

12
@RobertHarvey我熟悉该论点,并且在理论上听起来很棒。作为对象设计者,您无法预见人们如何扩展您的类,这就是为什么应该密封他们的原因。您不能支持所有内容-很好。别。但也不要放弃选择。
Michael

6
@RobertHarvey不仅仅是打破LSP的人得到了他们应得的东西吗?
StuperUser 2012年

2
我也不是密封类的忠实拥护者,但是我可以理解为什么为什么像Microsoft这样的公司(经常不得不支持他们不会破产的事情)会发现它们有吸引力。框架的用户不一定要了解类的内部知识。
罗伯特·哈维

11

埃里克·利珀特(Eric Lippert 的精彩文章,但我认为它不支持您的观点。

他认为,所有公开供他人使用的类都应被密封,或以其他方式使其不可扩展。

您的前提是所有类都应该是抽象的或密封的。

巨大差距。

EL的文章并未提及您和我一无所知的他的团队制作的(大概有很多)课程。通常,框架中公开的类仅是实现该框架所涉及的所有类的子集。


但是,您可以将相同的参数应用于不属于公共接口的类。使课程最终定下来的主要原因之一是,它作为防止草率代码的安全措施。该参数对于不同开发人员随时间共享的任何代码库同样有效,即使您是唯一的开发人员也是如此。它只是保护代码的语义。
DPM

我认为类应该是抽象或密封的想法是更广泛原则的一部分,即应该避免使用可实例化的类类型的变量。这种避免将使得可以创建给定类型的消费者可以使用的类型,而不必继承其所有私有成员。不幸的是,这样的设计不能真正使用public-constructor语法。相反,代码必须替换new List<Foo>()List<Foo>.CreateMutable()
超级猫

5

从Java的角度来看,我认为最终类并不像看起来那样聪明。

许多工具(尤其是AOP,JPA等)都可以处理加载时间,因此必须扩展您的专长。另一种方法是创建委托(而不是.NET委托),将所有内容都委托给原始类,这比扩展用户类要麻烦得多。


5

需要使用普通的非密封类的两种常见情况:

  1. 技术的:如果您的层次结构深度超过两个层次,并且希望能够在中间实例化某些内容。

  2. 原理:有时最好编写明确设计为安全扩展的类。(在编写像Eric Lippert这样的API时,或者在团队中处理大型项目时,这种情况经常发生。)有时您想编写一个本身可以正常工作的类,但在设计时要考虑到可扩展性。

埃里克·利珀特(Eric Lippert)关于密封的想法是有道理的,但他也承认,通过将类保持“开放”状态,它们确实为可扩展性而设计。

是的,BCL中密封了许多类,但是没有很多类,并且可以通过各种奇妙的方式进行扩展。我想到的一个例子是Windows窗体,您可以在其中Control通过继承将数据或行为添加到几乎所有窗体或窗体中。当然,这可以通过其他方式(装饰器模式,各种类型的合成等)完成,但是继承也可以很好地工作。

.NET的两个特定说明:

  1. 在大多数情况下,密封类通常对安全性不是很关键,因为virtual除了显式接口实现之外,继承者都不会破坏您的非功能性。
  2. 有时,一种合适的替代方法是构造方法internal而不是密封类,从而使它可以在代码库内部而不是在代码库外部进行继承。

4

我相信许多人认为课程应该final/ 的真正原因sealed是,大多数非抽象的可扩展课程没有得到适当的记录

让我详细说明。从远处开始,一些程序员认为,作为OOP中的一种工具,继承被广泛使用和滥用。我们都读过Liskov替代原则,尽管这并没有阻止我们违反它数百次(甚至数千次)。

问题是程序员喜欢重用代码。即使这不是一个好主意。继承是这种“重新编码”的关键工具。返回到最后/密封的问题。

最终/密封类的适当文档相对较少:您描述每种方法的功能,参数,返回值等。所有常用的东西。

但是,在正确记录可扩展类时,必须至少包括以下内容

  • 方法之间的依赖关系(哪个方法调用哪个,等等)
  • 对局部变量的依赖
  • 扩展类应遵守的内部合同
  • 每个方法的调用约定(例如,当您重写它时,您调用super实现吗?是在方法的开始还是结束时调用它?考虑构造函数与析构函数)
  • ...

这些只是我的头上。我可以为您提供一个示例,说明为什么每个元素都很重要,而跳过这些元素将使扩展类搞砸。

现在,考虑应该为正确记录每件事做多少记录工作。我相信具有7-8个方法的类(在理想化的世界中可能太多了,但实际上却太少了)可能还包含约5页的纯文本文档。因此,取而代之的是,我们半途而废,不给全班同学上课,以便其他人可以使用它,但也不能正确记录下来,因为这将花费大量时间(而且,您可能会无论如何都永远不会扩展,那么为什么要打扰呢?)。

如果您正在设计一门课程,您可能会感到有一种将其密封的诱惑,以使人们无法以您未曾预见(和准备)的方式使用它。另一方面,当您使用别人的代码时,有时从公共API使用该类,则没有明显的理由使该类最终定下来,您可能会认为“该死,这只花了我30分钟的时间来寻找解决方法”。

我认为解决方案的一些要素是:

  • 首先,当您是代码的客户端时,确保扩展是一个好主意,并且真正偏向于继承而不是继承。
  • 其次,要完整阅读本手册(再次作为客户),以确保您不会忽略所提到的内容。
  • 第三,在编写客户端将使用的代码时,请为代码编写适当的文档(是的,很长的路要走)。举一个积极的例子,我可以提供Apple的iOS文档。它们不足以使用户始终正确扩展其类,但是它们至少包括一些有关继承的信息。对于大多数API来说,这比我能说的还多。
  • 第四,实际尝试扩展您自己的类,以确保它能正常工作。我大力支持在API中包含许多示例和测试,并且在进行测试时,您最好还是测试继承链:毕竟它们是合同的一部分!
  • 第五,在您有疑问的情况下,请指示该类不是要扩展的,并且这样做不是一个好主意(tm)。表示您不应对此类意外使用负责,但仍不要对全班负责。显然,这不包括将类100%密封的情况。
  • 最后,在密封类时,提供一个接口作为中间钩子,以便客户端可以“重写”自己的类的修改版本并解决“密封”类。这样,他可以用其实现替换密封的类。现在,这应该很明显了,因为它以最简单的形式是松散耦合,但是仍然值得一提。

还值得一提下面的“哲学”问题:是一类是sealed/ final合同的类,或者实现细节的一部分?现在我不想踩到那里,但是对此的答案也将影响您决定是否上课的决定。


3

如果出现以下情况,则该类既不应是最终的/封闭的,也不应该是抽象的:

  • 它本身很有用,即拥有该类的实例是有益的。
  • 该类成为其他类的子类/基类是有益的。

例如,使用ObservableCollection<T>C#中。它只需要将事件的引发添加到a的常规操作中Collection<T>,这就是它子类化的原因Collection<T>Collection<T>它本身就是一个可行的类,因此ObservableCollection<T>


如果我负责此工作,我可能会做CollectionBase(抽象),Collection : CollectionBase(密封),ObservableCollection : CollectionBase(密封)。如果仔细查看Collection <T>,您会发现它是一个半确定的抽象类。
尼古拉斯·瑞皮凯特

2
您拥有三个班级而不是两个班级,您将获得什么优势?另外,Collection<T>“半辅助抽象类”又如何呢?
FishBasketGordo 2012年

Collection<T>通过受保护的方法和属性公开了许多内脏,并且显然打算成为专门收集的基类。但是尚不清楚应该将代码放在哪里,因为没有抽象方法可以实现。ObservableCollection继承自Collection并且未密封,因此您可以再次继承它。而且您可以访问Items受保护的属性,从而可以在引发事件的情况下添加集合中的项目。很好。
Nicolas Repiquet 2012年

@NicolasRepiquet:在许多语言和框架中,创建对象的常规习惯方法要求将保存对象的变量的类型与所创建实例的类型相同。在许多情况下,理想的用法是将引用传递给抽象类型,但这将迫使许多代码在调用构造函数时使用一种类型的变量和参数,而使用另一种具体类型。几乎不可能,但是有点尴尬。
2012年

3

最终/密封类的问题在于,他们正在尝试解决尚未发生的问题。仅当问题存在时它才有用,但由于第三方施加了限制,这令人沮丧。密封等级很少解决当前的问题,这使得很难说出它的有用性。

在某些情况下,应该密封一堂课。例如 该类以无法预测将来的更改如何改变该管理的方式来管理分配的资源/内存。

多年来,我发现封装,回调和事件比抽象类更加灵活/有用。我看到了很多具有大型类层次结构的代码,其中封装和事件会使开发人员的生活变得更简单。


1
软件中的最终密封解决了“我认为有必要将我的意志强加给该代码的未来维护者”的问题。
卡兹(Kaz)2012年

不是最终的/密封的东西添加到OOP中,因为我不记得它在我年轻的时候就出现了。这似乎是一种经过深思熟虑的功能。
Reactgular 2012年

在OOP被诸如C ++和Java之类的语言愚弄之前,终结处理作为OOP系统中的工具链过程存在。程序员可以在Lisp的Smalltalk和Lisp中最大程度地进行工作:可以扩展任何内容,并始终添加新方法。然后,对系统的编译图像进行优化:假设最终用户不会扩展该系统,因此这意味着可以基于盘点哪些方法和方法来优化方法分配。类现在存在。
卡兹(Kaz)2012年

我认为这不是一回事,因为这只是一项优化功能。我不记得在所有版本的Java中都是密封的,但是由于使用不多,我可能会错了。
Reactgular 2012年

仅仅因为它表现为您必须在代码中添加的一些声明,并不意味着它不是一回事。
卡兹(Kaz)2012年

2

对象系统中的“密封”或“最终确定”允许进行某些优化,因为已知完整的调度图。

就是说,作为性能的折衷,我们很难将系统更改为其他内容。(这是大多数优化的本质。)

在所有其他方面,这都是一种损失。系统默认情况下应该是开放且可扩展的。向所有类添加新方法并任意扩展应该很容易。

通过采取措施防止将来的扩展,我们今天无法获得任何新功能。

因此,如果我们为了预防本身而这样做,那么我们正在做的就是试图控制未来维护者的生活。关于自我。“即使我不再在这里工作,该代码也将按照我的方式维护,该死!”


1

测试类似乎浮现在脑海。这些是基于程序员/测试人员试图完成的工作而以自动化方式或“随意”调用的类。我不确定我是否曾见过或听说过私人完成的测试。


你在说什么 考试必须采用哪种方式才能最终/封闭/抽象?

1

当您需要为将来做一些实际的计划时,需要考虑打算扩展的课程。让我给你一个现实生活中的例子。

我花了大量时间在主要产品和外部系统之间编写接口工具。当我们进行新的销售时,一个重要组成部分是一组出口商,这些出口商被设计为定期运行,它们生成详细描述当天发生的事件的数据文件。这些数据文件然后由客户的系统使用。

这是扩展课程的绝佳机会。

我有一个Export类,它是每个出口商的基础类。它知道如何连接到数据库,找出上次运行该数据库的位置,并为其生成的数据文件创建存档。它还提供属性文件管理,日志记录和一些简单的异常处理。

最重要的是,我有一个不同的导出器来处理每种类型的数据,也许有用户活动,交易数据,现金管理数据等。

在此堆栈的顶部,我放置了一个特定于客户的层,该层实现了客户所需的数据文件结构。

这样,基本出口商很少更改。核心数据类型导出器有时会更改,但很少更改,通常仅用于处理数据库架构更改,无论如何应将这些更改传播给所有客户。我为每个客户要做的唯一工作就是特定于该客户的那部分代码。完美的世界!

所以结构看起来像:

Base
 Function1
  Customer1
  Customer2
 Function2
 ...

我的主要观点是,通过以这种方式设计代码,我可以将继承主要用于代码重用。

我不得不说,我想不出任何理由要超过三层。

我已经使用了两层,例如,有一个通用Table类用于实现数据库表查询,而子类则Table通过使用enum定义字段来实现每个表的特定细节。让enum实现器在Table类中定义一个接口具有各种意义。


1

我发现抽象类很有用,但并非总是需要,密封类在应用单元测试时成为一个问题。除非使用Teleriks justmock之类的东西,否则不能模拟或存根密封类。


1
这是一个很好的评论,但不能直接回答问题。请考虑在这里扩大您的想法。

1

我同意你的观点。我认为在Java中,默认情况下,类应声明为“最终”。如果您没有将其定稿,请专门进行准备并记录以进行扩展。

这样做的主要原因是要确保您的类的任何实例都将遵守您最初设计和记录的接口。否则,使用您的类的开发人员可能会创建脆弱且不一致的代码,进而将其传递给其他开发人员/项目,从而使类的对象不可信。

从更实际的角度来看,这确实有一个缺点,因为库界面的客户端将无法进行任何调整,而无法以您最初想到的更灵活的方式使用您的类。

就个人而言,对于存在的所有质量差的代码(并且由于我们在更实际的水平上进行讨论,因此我认为Java开发更容易这样做),我认为这种刚性对于我们寻求更容易实现的目标来说是一个很小的代价。维护代码。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.