为什么不应该将类设计为“开放式”?


44

在阅读各种Stack Overflow问题和其他代码时,关于如何设计类的一般共识已经关闭。这意味着默认情况下,在Java和C#中,所有内容都是私有的,字段是final,某些方法是final,有时类甚至是final

这背后的想法是隐藏实现细节,这是一个很好的理由。但是,由于protected大多数OOP语言和多态性的存在,这是行不通的。

每当我想为类添加或更改功能时,通常都会受到私有和最终放置在任何地方的阻碍。这里的实现细节很重要:您正在实施并扩展它,完全了解其后果。但是,由于无法访问私有字段和最终字段和方法,因此我有以下三种选择:

  • 不要扩展类,只解决导致代码更复杂的问题
  • 复制并粘贴整个类,杀死代码的可重用性
  • 分叉项目

这些不是很好的选择。为什么protected在用支持它的语言编写的项目中不使用它?为什么某些项目明确禁止从类继承?


1
我同意,我在Java Swing组件中遇到了这个问题,这确实很糟糕。
乔纳斯(Jonas)


3
如果您遇到此问题,很有可能一开始就设计不好的类,或者您可能试图不正确地使用它们。您不需要向班级索取信息,而是要求班级为您做些事情-因此,通常您不需要该数据。尽管并非总是如此,但如果您发现需要访问课程数据,则很可能出现了问题。
Bill K

2
“大多数OOP语言?” 我知道很多无法关闭类的地方。您没有选择第四个选项:更改语言。
凯文·克莱恩

@Jonas Swing的主要问题之一是它公开了太多的实现。这确实扼杀了发展。
Tom Hawtin-大头钉

Answers:


55

设计类以使其在扩展时可以正常工作,尤其是当程序员进行扩展时不能完全理解该类应如何工作时,这需要花费大量的额外精力。您不能只是将所有私有的东西都公开(或保护)并称之为“公开”。如果允许其他人更改变量的值,则必须考虑所有可能的值将如何影响类。(如果他们将变量设置为null,该怎么办?一个空数组?一个负数?)在允许其他人调用方法时也是如此。需要仔细考虑。

因此,不应该打开类不是很多,但是有时不值得将它们打开。

当然,图书馆作者也可能只是懒惰。取决于您所讨论的库。:-)


13
是的,这就是默认情况下密封类的原因。因为您无法预测客户扩展类的许多方式,所以密封它比承诺支持可能无限数量的类扩展方法更安全。
罗伯特·哈维


18
您不必预测班级将如何被改写,您只需要假设扩展班级的人们都知道自己在做什么。如果他们扩展了类,并在不应将其设置为null的情况下将其设置为null,则是他们的错,而不是您的错。该参数唯一有意义的地方是在超临界应用程序中,如果字段为null,则某些地方将发生严重错误。
TheLQ

5
@TheLQ:这是一个很大的假设。
罗伯特·哈维

9
@TheLQ是谁的过错是一个非常主观的问题。如果他们将变量设置为看似合理的值,并且您没有记录不允许这样做的事实,并且您的代码没有引发ArgumentException(或等效变量),但这会导致服务器脱机或导致对安全漏洞的利用,那么我想说这跟他们一样是你的错。而且,如果您确实记录了有效值并进行了参数检查,那么您就已经提出了我正在谈论的那种工作,并且您必须问自己,这种工作是否真的是在浪费时间。
亚伦

24

默认情况下将所有内容设为私有听起来很苛刻,但从另一面来看:当默认情况下所有内容均为私有时,将某些内容设为公开(或受保护,几乎是同一件事)应该是一种有意识的选择。这是班级作者与您(消费者)之间关于如何使用该班级的合同。这对你们俩都是一个方便:只要接口保持不变,作者就可以自由地修改类的内部工作。并且您确切地知道您可以依赖该课程的哪些部分,以及哪些部分可能会发生变化。

基本思想是“松耦合”(也称为“窄接口”);它的价值在于降低复杂性。通过减少组件交互的方式数量,它们之间的相互依赖性也减少了。在维护和变更管理方面,交叉依赖是最糟糕的复杂性之一。

在设计良好的库中,值得通过继承扩展的类将保护和公共成员放在正确的位置,并隐藏了其他所有内容。


确实有道理。但是,当您需要非常相似但实现中有1或2项更改(类的内部工作)时,仍然存在问题。我还很难理解,如果您重写了该类,您是否已经严重依赖于其实现了吗?
TheLQ 2011年

5
+1为松散耦合带来的好处。@TheLQ,它是您应该高度依赖的接口,而不是实现。
Karl Bielefeldt

19

所有私有的东西或多或少都应该在该类的每个以后版本中以不变的方式存在。实际上,它可以被认为是API的一部分,无论有没有文档。因此,暴露过多的细节可能会在以后引起兼容性问题。

关于“最终”回应。“密封”类是不可变类,一种典型的情况是。框架的许多部分都依赖于字符串是不可变的。如果String类不是最终的,那么创建一个可变的String子类(而不是不可变的String类)会很容易(甚至很诱人),该子类会在许多其他类中引起各种错误。


4
这是真正的答案。其他答案涉及到重要的事情,但真正重要的一点是:所有未final和/或private锁定在位 且永远无法更改的事物
康拉德·鲁道夫

1
@Konrad:直到开发人员决定修改所有内容并且不向后兼容为止。
JAB

的确如此,但值得指出的是,这一要求比对public会员的要求要弱得多。protected成员仅在暴露它们的类与其直接派生的类之间形成合同;相反,public成员代表所有可能的现在和将来继承的类与所有消费者签订合同。
2012年

14

在OO中,有两种向现有代码添加功能的方法。

第一个是通过继承:您可以继承一个类并从中派生。但是,继承应谨慎使用。主要应在基类和派生类之间具有isA关系(例如Rectangle Shape)时使用公共继承。相反,您应该避免公共继承来重用现有的实现。基本上,公共继承用于确保派生类具有与基类相同的接口(或更大的接口),以便您可以应用Liskov的替换原理

添加功能的另一种方法是使用委托或组合。您编写自己的类,将现有类用作内部对象,并将部分实现工作委托给它。

我不确定您要使用的库中所有这些决赛的背后的主意是什么。程序员可能想确保您不会从他们的类继承新类,因为这不是扩展其代码的最佳方法。


5
构图与继承的重点。
ZsoltTörök

+1,但我从不喜欢继承的“形状”示例。矩形和圆形可能是两个截然不同的事物。我更喜欢使用,正方形是受约束的矩形。矩形可以像形状一样使用。
tylermac

@tylermac:的确如此。Square / Rectangle情况实际上是LSP的反例之一。也许我应该开始考虑一个更有效的例子。
knulp 2011年

3
如果允许修改,则Square / Rectangle仅是一个反例。
starblue 2011年

11

大多数答案都是正确的:如果没有设计或打算扩展对象,则应将类密封(final,NotOverridable等)。

但是,我不会考虑简单地关闭所有适当的代码设计。SOLID中的“ O”表示“开放式封闭原则”,表示应“封闭”类以进行修改,而“开放”以扩展。这个想法是,您在对象中有代码。它工作正常。添加功能不应要求打开该代码并进行可能会破坏先前工作行为的手术更改。取而代之的是,人们应该能够使用继承或依赖性注入来“扩展”该类以执行其他操作。

例如,类“ ConsoleWriter”可以获取文本并将其输出到控制台。它做得很好。但是,在某些情况下,您还需要将相同的输出写入文件。通常,打开ConsoleWriter的代码,更改其外部接口以向主要功能添加参数“ WriteToFile”,然后将其他文件编写代码放在控制台编写代码旁边,这通常被认为是一件坏事。

相反,您可以执行以下两项操作之一:您可以从ConsoleWriter派生以形成ConsoleAndFileWriter,并扩展Write()方法以首先调用基本实现,然后再写入文件。或者,您可以从ConsoleWriter中提取一个接口IWriter,然后重新实现该接口以创建两个新类。一个FileWriter和一个“ MultiWriter”,可以将其分配给其他IWriter,并将“广播”对其方法的所有调用给所有给定的Writer。您选择哪种取决于您的需求。简单的推导很好,但是更简单,但是如果您最终无法将消息发送到网络客户端,三个文件,两个命名管道和控制台有很模糊的预见,请继续尝试以提取接口并创建接口。 “ Y适配器”;它'

现在,如果从未将Write()函数声明为虚拟函数(或被密封),那么如果您不控制源代码,那么您将遇到麻烦。有时即使您这样做。通常这是一个不好的位置,它会使闭源API或受限源API的用户无休止地无所适从。但是,有合理的原因将Write()或整个类ConsoleWriter密封起来;在受保护的字段中可能存在敏感信息(又从基类中覆盖以提供所述信息)。验证可能不够;当您编写一个api时,您必须假设使用您代码的程序员并不比最终系统的平均“最终用户”更聪明或更仁慈。这样您就不会失望。


好点子。继承可以是好事也可以是坏事,因此说每个类都应该密封是错误的。这确实取决于眼前的问题。
knulp 2011年

2

我认为可能是所有使用oo语言进行c编程的人。我在看你,java。如果锤子的形状像一个对象,但是您需要一个过程系统,并且/或者不真正理解类,那么您的项目将需要锁定。

基本上,如果您打算使用oo语言进行编写,则您的项目需要遵循oo范式,其中包括扩展。当然,如果有人用不同的范例编写了一个库,也许您不应该继续扩展它。


7
BTW,OO和程序在大多数情况下不是相互排斥的。没有程序就不能拥有OO。同样,组合与扩展一样是面向对象的概念。
Michael K

@Michael当然不是。我说过,您可能需要一个过程系统,即没有OO概念的系统。我见过人们使用Java编写纯粹的过程代码,如果您尝试以OO方式与之交互,它将无法正常工作。
Spencer Rathbun

1
如果Java具有一流的功能,那可以解决。嗯
Michael K'7

向新生学习Java或DOTNet语言是很困难的。我通常使用过程Pascal或“ Plain C”来教授过程式,后来,使用对象Pascal或“ C ++”来切换到OO。并且,将Java留给以后使用。使用过程式程序进行教学编程看起来像单个对象(单例)效果很好。
umlcat 2011年

@Michael K:在OOP中,一流的功能只是具有一个方法的对象。
Giorgio 2013年

2

因为他们没有更好的了解。

原始作者可能会误解对SOLID原理的误解,而SOLID原理源于一个混乱而复杂的C ++世界。

我希望您会注意到ruby,python和perl世界没有问题,这里的答案声称是密封的原因。请注意,它与动态类型正交。访问修饰符很容易在大多数(所有?)语言中使用。可以通过强制转换为其他类型来破坏C ++字段(C ++较弱)。Java和C#可以使用反射。除非确实需要,否则访问修饰符会使事情变得非常困难,足以阻止您执行此操作。

密封类并将任何成员标记为私有显然违反了简单事物应该简单而硬事物应该可能的原则。突然之间,应该简单的事情并非如此。

我鼓励您尝试理解原始作者的观点。它的很大一部分来自封装的学术思想,但从未在现实世界中证明绝对成功。我从未见过某个框架或库,那里的某个开发人员不希望它的工作原理略有不同,并且没有充分的理由对其进行更改。最初的软件开发人员可能会遇到两种可能性,即密封了成员并使成员私有化。

  1. 傲慢自大-他们确实相信自己可以扩展并拒绝修改
  2. 自满情绪-他们知道可能还有其他用例,但决定不为这些用例编写

我认为在公司框架中,#2可能就是这种情况。这些C ++,Java和.NET框架必须“完成”,并且必须遵循某些准则。这些准则通常指的是密封类型,除非类型被明确设计为类型层次结构的一部分,并且私有成员用于很多事情,这可能对其他人有用。.但是由于它们与该类没有直接关系,因此不会暴露。提取新类型对于支持,文档等来说太昂贵了。

访问修饰符背后的整个想法是,程序员应该受到保护。“ C编程是不好的,因为它使您无法自拔。” 作为程序员,我不认同这种哲学。

我非常喜欢python的名称处理方法。如果需要,您可以轻松地(比反射容易得多)替换私有。可以在这里找到有关它的出色文章:http : //bytebaker.com/2009/03/31/python-properties-vs-java-access-modifiers/

Ruby的private修饰符实际上更像在C#中受保护,并且没有private作为C#修饰符。受保护有点不同。这里有很好的解释:http : //www.ruby-lang.org/en/documentation/ruby-from-other-languages/

请记住,您的静态语言不必遵循过去用该语言编写的代码的过时样式。


5
“封装从未证明绝对成功”。我本以为每个人都会同意,缺乏封装已经造成了很多麻烦。
荷兰Joh

5
-1。愚人奔向天使惧怕的地方。庆祝更改私有变量的能力表明您的编码风格存在严重缺陷。
riwalk

2
除了值得商and和可能是错误的之外,此贴子也相当傲慢和侮辱。做得很好。
康拉德·鲁道夫

1
有两种思想流派,其支持者似乎相处不融洽。静态类型的人希望易于对软件进行推理,动态类型的人希望易于添加功能。哪个更好,基本上取决于项目的预期寿命。
荷兰Joh

1

编辑:我认为课程应该设计为开放的。有一些限制,但不应因继承而关闭。

原因:对我来说,“最终班”或“密封班”似乎很奇怪。我永远不必将自己的一个班级标记为“最终”(“密封”),因为稍后可能需要在该班级中设置班级。

我已经购买/下载了带有类的第三方库((大多数视觉控件)),并且非常感谢这些库中没有一个使用“最终”库,即使它们在其中进行了编码的编程语言支持也是如此,因为我结束了对这些类的扩展,即使我已经编译了没有源代码的库。

有时,我不得不处理一些偶然的“密封”类,然后结束包含给定类的具有类似成员的新类“包装”。

class MyBaseWrapper {
  protected FinalClass _FinalObject;

  public virtual DoSomething()
  {
    // these method cannot be overriden,
    // its from a "final" class
    // the wrapper method can  be open and virtual:
    FinalObject.DoSomething();
  }
} // class MyBaseWrapper


class MyBaseWrapper: MyBaseWrapper {

  public override DoSomething()
  {
    // the wrapper method allows "overriding",
    // a method from a "final" class:
    DoSomethingNewBefore();
    FinalObject.DoSomething();
    DoSomethingNewAfter();
  }
} // class MyBaseWrapper

范围分类器可以“密封”某些功能,而不会限制继承。

当我从结构化进程切换时。在OOP中,我开始使用私有类成员,但是在许多项目之后,我结束了使用protected,并最终将这些属性或方法推广到public(如果需要)。

PS我不喜欢C#中的“ final”作为关键字,而是在继承隐喻之后使用Java之类的“ sealed”或“ sterile”。此外,“最终”是在多种情况下使用的歧义词。


-1:您说过自己喜欢开放式设计,但这不能回答问题,这就是为什么不应将类设计为开放式。
荷兰Joh

@Joh对不起,我并没有很好地解释自己,我试图表达相反的意见,不应该被封印。感谢您描述为什么您不同意。
umlcat 2011年
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.