为什么许多软件开发人员违反开放/封闭原则?


74

为什么许多软件开发人员通过修改诸如重命名功能之类的东西在升级后会破坏应用程序而违反了开放/关闭原则

React库中出现快速和连续版本之后,这个问题就跳到了我的头上。

每隔一小段时间,我就会注意到语法,组件名称等方面的许多更改。

即将发布的React版本中的示例:

新的弃用警告

最大的变化是我们将React.PropTypes和React.createClass提取到了自己的包中。两者仍然可以通过主React对象访问,但是在开发模式下,使用其中任何一个都会将一次性弃用警告记录到控制台。这将使将来的代码大小优化成为可能。

这些警告将不会影响您的应用程序的行为。但是,我们意识到它们可能会引起一些挫败感,特别是如果您使用将console.error视为失败的测试框架。


  • 这些更改是否被视为违反该原则?
  • 作为React之类的初学者,我如何通过库中的这些快速更改来学习它(这是如此令人沮丧)?

6
这显然是观察它的一个例子,您的“那么多”主张没有根据。Lucene和RichFaces项目是臭名昭著的示例,还有Windows COMM端口API,但我想不到其他任何东西。React真的是“大型软件开发商”吗?
user207421 '17

62
像任何原理一样,OCP也有其价值。但这要求开发人员具有无限的远见。在现实世界中,人们常常会误解第一个设计。随着时间的流逝,为了兼容起见,有些人更喜欢解决他们的旧错误,而另一些人则为了拥有紧凑而轻松的代码库而最终清除它们。
Theodoros Chatzigiannakis'5

1
上次您什么时候看到了“原本打算”的面向对象语言?核心原则是消息传递系统,这意味着系统的每个部分都可以被任何人无限扩展。现在,将其与典型的类似OOP的语言进行比较-有多少种可以让您从外部扩展现有方法?有多少使它变得足够容易变得有用?
Lu安

传统糟透了。30年的经验表明,您应该始终完全放弃旧的并重新开始。今天,每个人在任何时候都可以随时随地联系在一起,因此,遗产与今天完全无关。最终的例子是“ Windows vs Mac”。微软传统上试图“支持旧版”,您可以通过多种方式看到这一点。苹果一直向老用户说“ F---You”。(这适用于从语言到设备到操作系统的所有内容。)实际上,Apple完全正确,MSFT完全错误,简单明了。
Fattie

4
因为在现实生活中,完全有100%的时间使用零“原理”和“设计模式”。
Matti Virkkunen

Answers:


148

IMHO JacquesB的回答虽然包含许多事实,但显示出对OCP的根本误解。公平地说,您的问题也已经表达了这种误解-重命名功能破坏了向后兼容性,但没有破坏OCP。如果似乎需要打破兼容性(或维护同一组件的两个版本以不破坏兼容性),则OCP之前已被破坏!

正如JörgW Mittag在其评论中已经提到的那样,该原则并没有说“您不能修改组件的行为”,而是说,应该尝试以开放的方式设计组件以供蜜蜂重复使用(或扩展)。以多种方式,无需修改。这可以通过提供正确的“扩展点”来完成,或者如@AntP所述,“通过将类/函数结构分解为默认情况下每个自然扩展点都存在的点”来完成。遵循OCP的恕我直言与“保持旧版本不变以实现向后兼容”没有任何共同点!或者,在下面引用@DerekElkin的评论:

OCP是有关如何编写模块的建议,而不是有关实现永远不允许模块更改的变更管理过程的建议。

优秀的程序员利用他们的经验来设计组件时要考虑“正确的”扩展点(或者,甚至更好的是,不需要人为的扩展点)。但是,要正确执行此操作且没有不必要的过度设计,您需要事先了解组件的未来用例会是什么样子。即使是经验丰富的程序员也无法展望未来,也无法事先了解所有即将出现的需求。这就是为什么有时需要向后兼容的原因-无论您的组件具有多少个扩展点,或者在某些类型的要求上遵循OCP的程度如何,总会有一个要求,如果不进行修改就无法轻松实现。组件。


14
IMO“违反” OCP的最大原因是要花费大量精力才能正确地遵守它。埃里克·利珀特(Eric Lippert)在一篇出色的博客文章中介绍了为什么许多.NET框架类似乎违反了OCP。
BJ Myers

2
@BJMyers:感谢您的链接。乔恩·斯凯特(Jon Skeet)关于OCP 的精彩文章与保护变体的想法非常相似。
布朗

8
这个!OCP表示您应该编写可以被更改而不会被触及的代码!为什么?因此,您只需要测试,审查和编译一次即可。新行为应来自新代码。不是通过拧入旧的经过验证的代码。重构呢?好的重构显然违反了OCP!这就是为什么编写代码以为您会在假设改变的情况下重新构建代码就是罪过。没有!将每个假设放在自己的小盒子中。错了,不要修理盒子。写一个新的。为什么?因为您可能需要回到旧的。当您这样做时,如果它仍然有效,那就太好了。
candied_orange

7
@CandiedOrange:感谢您的评论。正如您所描述的,我没有看到重构和OCP如此相反。要编写遵循OCP的组件,通常需要几个重构周期。目标应该是不需要修改即可解决整个“系列”需求的组件。但是,不应“以防万一”在组件上添加任意扩展点,这很容易导致过度设计。在许多情况下,依靠重构的可能性可能是更好的选择。
布朗

4
这个答案很好地指出了(当前)最佳答案中的错误-我认为,尽管成功完成了开/关操作,关键是要停止思考“扩展点”,而开始考虑分解您的类/函数结构到默认情况下每个自然扩展点都存在的位置。编程“在外面”是实现这一目标的一个很好的方式,每一个场景当前的方法/功能迎合推到外部接口,这构成了装饰的自然延伸点,适配器等等
蚂蚁P

67

打开/关闭原理有好处,但也有一些严重的缺点。

从理论上讲,该原理通过创建“可扩展但不可修改”的代码来解决向后兼容的问题。如果类有一些新要求,则您永远不要修改类本身的源代码,而是创建一个子类,该子类仅覆盖更改行为所需的适当成员。因此,针对该类的原始版本编写的所有代码均不受影响,因此您可以确信所做的更改不会破坏现有代码。

实际上,您很容易陷入代码膨胀和混乱的过时类混乱之中。如果无法通过扩展修改组件的某些行为,则必须提供具有所需行为的组件新变体,并保持旧版本不变以实现向后兼容性。

假设您在许多类都继承自的基类中发现了一个基本的设计缺陷。说该错误是由于私有字段的类型错误引起的。您无法通过覆盖成员来解决此问题。基本上,您必须重写整个类,这意味着您最终需要扩展Object以提供替代的基类-现在您还必须为所有子类提供替代方案,从而最终导致重复的对象层次结构,一个有缺陷的层次结构,一个改进的层次结构。但是您不能删除有缺陷的层次结构(因为删除代码是修改的),所有将来的客户端都将同时处于这两个层次结构中。

现在,对该问题的理论答案是“仅在第一时间正确设计”。如果代码被完美地分解,没有任何缺陷或错误,并且设计有针对将来所有可能的需求更改而准备的扩展点,则可以避免混乱。但实际上,每个人都会犯错,没有人能完美预测未来。

像.NET框架这样的东西-它仍然包含了集合类的集合,这些集合类是在十多年前引入泛型之前设计的。这无疑是向后兼容的福音(您可以升级框架而不必重写任何内容),但是它也使框架膨胀,并为开发人员提供了很多选择,其中很多选择已过时。

显然,React的开发人员认为,严格遵循开放/封闭原则不值得在复杂性和代码膨胀方面付出代价。

实用的替代方法是控制弃用。不会在单个发行版中破坏向后兼容性,而是将旧组件保留一个发行周期,但是会通过编译器警告通知客户端,该旧方法将在以后的发行版中删除。这使客户有时间修改代码。在这种情况下,这似乎是React的方法。

(我对该原则的解释是基于罗伯特·C·马丁(Robert C. Martin)的《开放-封闭原则》


37
“原则上说,您不能修改组件的行为。相反,您必须提供具有所需行为的组件新变体,并保持旧版本不变以实现向后兼容。” –我不同意这一点。该原则说,您应该以不必更改其行为的方式设计组件,因为您可以扩展它以执行您想要的事情。问题在于,我们还没有弄清楚该怎么做,特别是对于目前广泛使用的语言。表达问题是一个组成部分...
约尔格W¯¯米塔格

8
……例如。Java和C♯都没有Expression的解决方案。Haskell和Scala可以这样做,但是他们的用户群要小得多。
约尔格W¯¯米塔格

1
@乔治:在Haskell中,解决方案是类型类。在Scala中,解决方案是隐式和对象。抱歉,目前没有链接。是的,多重方法(实际上,它们甚至不需要“多重”,而是需要Lisp方法的“开放”性质)也是可能的解决方案。请注意,表达问题有多种表述,因为通常情况下,论文的撰写方式是作者对表达问题增加了限制,从而导致当前所有现有解决方案均无效,然后说明了自己的解决方案……
JörgW Mittag

1
……语言甚至可以解决这个“更难”的版本。例如,Wadler最初将表达问题表述为不仅涉及模块化扩展,而且涉及静态安全的模块化扩展。但是,常见的Lisp多方法不是静态安全的,它们只是动态安全的。然后,Odersky通过说它应该是模块化的静态安全模块来进一步加强这一点,也就是说,仅通过查看扩展模块,就可以在不查看整个程序的情况下对安全性进行静态检查。实际上,这不能使用Haskell类型类完成,但是可以使用Scala完成。而在...
约尔格W¯¯米塔格

2
@Giorgio:是的。使Common Lisp多方法解决EP的事情实际上不是多重调度。事实上,这些方法是开放的。在典型的FP(或过程编程)中,类型区分与功能相关。在典型的OO中,方法与类型相关。常见的Lisp方法是开放的,可以在事实发生后将它们添加到类中并在其他模块中。这就是使它们可用于解决EP的功能。例如,Clojure的协议是单调度,但也解决了EP(只要您不坚持静态安全性)。
约尔格W¯¯米塔格

20

我将开放/封闭原则称为理想。像所有理想一样,它几乎没有考虑软件开发的现实。同样,与所有理想一样,在实践中也无法真正实现它-一个人只是努力尽可能地实现这一理想。

故事的另一面被称为金手铐。当您过多地拥护开放/封闭原则时,您会得到金手铐。当您的产品永远不会向后兼容时,因为过去的错误太多,导致产品无法生长,就会出现金手铐。

Windows 95内存管理器中提供了一个著名的示例。作为Windows 95营销的一部分,据说所有Windows 3.1应用程序都可以在Windows 95中运行。Microsoft实际上获得了数千个程序的许可以在Windows 95中对其进行测试。问题之一就是Sim City。Sim City实际上存在一个错误,该错误导致其写入未分配的内存。在Windows 3.1中,如果没有“适当的”内存管理器,这是次要的问题。但是,在Windows 95中,内存管理器将捕获此错误并导致分段错误。解决方案?在Windows 95中,如果您的应用程序名称为simcity.exe,则操作系统实际上将放宽内存管理器的约束以防止分段错误!

这个理想背后的真正问题是产品和服务的精简概念。没人真正做到其中之一。一切都在两者之间的灰色区域中的某个地方排列。如果您从面向产品的角度考虑,打开/关闭听起来很理想。您的产品是可靠的。但是,涉及服务时,故事会发生变化。很容易证明,采用开放/封闭原则,您的团队必须支持的功能量必须渐近地趋近于无穷大,因为您永远无法清理旧的功能。这意味着您的开发团队每年必须支持越来越多的代码。最终,您达到了一个突破点。

如今,大多数软件,尤其是开源软件,都遵循开放/封闭原则的通用宽松版本。在次要版本中看到开/关紧随其后是很普遍的,但在主要版本中却被放弃。例如,Python 2.7包含了来自Python 2.0和2.1天的许多“错误选择”,但Python 3.0席卷了所有这些。(此外,当他们发布Windows 2000时,从Windows 95代码库向Windows NT代码库的转变打破了所有事情,但这确实意味着我们不必再与检查应用程序名称来决定行为的内存管理器打交道!)


那是关于SimCity的一个很棒的故事。你有资源吗?
BJ Myers

5
@BJMyers这是一个古老的故事,乔尔Spoleky提到它接近结束这篇文章。几年前,我最初将其作为有关开发视频游戏的书的一部分进行阅读。
Cort Ammon

1
@BJMyers:我很确定他们对许多流行的应用程序都具有类似的兼容性“ hacks”。
布朗

3
@BJMyers有很多类似的东西,如果您想阅读Raymond Chen的The Old New Thing博客,请浏览History标签或搜索“ compatibility”。人们回想起了很多故事,包括一些与上述《模拟城市》案例非常接近的故事-Addentum:Chen不喜欢指责名字。
Theraot '17

2
在95-> NT过渡中,几乎没有什么东西能收支平衡。Windows的原始SimCity在Windows 10(32位)上仍然可以正常运行。即使您禁用声音或使用VDMSound之类的东西来允许控制台子系统正确处理音频,即使DOS游戏仍然可以正常工作。微软非常重视向后兼容性,他们也不采用任何“让我们将其放入虚拟机”快捷方式。有时需要解决方法,但这仍然令人印象深刻,尤其是相对而言。
a安

11

布朗博士的答案最接近准确,其他答案则说明了对开放式封闭原则的误解。

要明确阐明误解,似乎有一个信念,即OCP意味着你不应该向后兼容的更改(甚至任何沿着这些路线的改变或什么的。)的OCP是如何设计的组件,这样你就不会需要到对其进行更改以扩展其功能,无论这些更改是否向后兼容。除了添加功能之外,还有许多其他原因,您可以对组件进行更改,无论它们是向后兼容的(例如,重构或优化)还是向后不兼容的(例如,过时和删除功能)。您可以进行这些更改并不意味着您的组件违反了OCP(并且绝对不意味着 违反了OCP)。

确实,这根本与源代码无关。OCP的一个更抽象和相关的陈述是:“组件应允许扩展而无需违反其抽象边界”。我将进一步讲,更现代的再现是:“组件应强制其抽象边界,但允许扩展”。甚至在鲍勃·马丁(Bob Martin)在OCP上发表的文章中,“将”接近修改的“描述为”源代码被违反“时,他后来也开始谈论封装,它与修改源代码无关,而与抽象有关边界。

因此,该问题的错误前提是,OCP(旨在作为)有关代码库演变的指南。OCP通常被口号为“一个组件应该对扩展开放,而对消费者不应该进行修改”。基本上,如果组件的使用者希望向该组件添加功能,则他们应该能够将旧组件扩展为具有附加功能的新组件,但他们应该不能更改旧组件。

OCP并没有说明更改删除功能的组件的创建者。OCP并不主张永远保持错误兼容性。作为创建者,您不会通过更改甚至删除组件来违反OCP。如果消费者可以向组件添加功能的唯一方法是通过对其进行变异(例如通过猴子补丁),则您或您编写的组件违反了OCP。或有权访问源代码并重新编译。在许多情况下,这些都不是消费者的选择,这意味着,如果您的组件没有“开放供扩展”,那么它们就倒霉了。他们根本无法使用您的组件来满足他们的需求。OCP争辩说,至少在某些可识别的“扩展名”方面,不要让您的图书馆的用户处于这种位置。即使可以对源代码甚至源代码的主副本进行修改,也最好“假装”您不能对其进行修改,因为这样做有很多潜在的负面影响。

因此,请回答您的问题:不,这些都不违反OCP。作者所做的任何更改都不能违反OCP,因为OCP并非更改的一部分。的变化,但是,可以创建违反OCP,他们可以通过在代码库的以前版本的OCP失败的动机。OCP是特定代码段的属性,而不是代码库的演进历史。

相比之下,向后兼容是代码更改的属性。说某些代码向后兼容或不向后兼容是没有道理的。谈论某些代码相对于某些较旧代码的向后兼容性才有意义。因此,谈论某些代码是否具有向后兼容性是没有道理的。代码的第一部分可以满足或不满足OCP,并且通常我们可以在不参考任何历史版本的情况下确定某些代码是否满足OCP。

关于最后一个问题,对于StackExchange来说,它基本上是基于观点的,因此通常可以说是离题的,但是对于技术(尤其是JavaScript)来说,它的简短之处是受欢迎的,在过去的几年中,您描述的现象被称为JavaScript疲劳。(可以随时通过Google查找各种其他文章,有些是讽刺性的,它们从多个角度进行讨论。)


3
“作为创建者,您不会通过更改甚至删除组件来违反OCP。” -您可以为此提供参考吗?我所见的原理定义都没有一个陈述“创建者”(无论是什么意思)都不受该原理约束。删除已发布的组件显然是一项重大更改。
JacquesB '17

1
@JacquesB人员甚至代码更改都不会违反OCP,而组件(即实际的代码段)则不会违反。(而且,非常清楚,这意味着该组件无法达到OCP本身的要求,而不是它违反了其他某些组件的OCP。)我的回答的全部重点是,OCP并不是在谈论代码更改。 ,损坏或其他原因。甲组件要么是分机打开和关闭,以修改或它不是,就像一个方法可以是private或不是。如果作者稍后提出private方法public,并不表示他们已经违反了访问控制权,(1/2)
Derek Elkins

2
...也不意味着该方法不是private以前真正的。毫无疑问,“删除已发布的组件显然是一项重大变化”。新版本的组件要么满足OCP,要么不满足OCP,您不需要代码库的历史来确定这一点。根据您的逻辑,我永远不会编写满足OCP的代码。您正在将向后兼容性(代码更改的属性)与OCP(代码属性)进行混淆。您的评论与说quicksort不向后兼容一样有意义。(2/2)
德里克·埃尔金斯

3
@JacquesB首先,请再次注意,这是在讨论符合OCP 的模块。OCP是有关如何编写模块的建议,以便不能更改源代码的限制下,仍然可以扩展该模块。在本文的较早部分,他谈到了设计永不更改的模块,而不是实现了永不更改模块的变更管理流程。参照对答案的编辑,您不会通过修改模块代码来“破坏OCP”。相反,如果“扩展”模块要求您修改源代码,则(1/3)
德里克·埃尔金斯

2
“ OCP是特定代码段的属性,而不是代码库的演变历史。” - 优秀的!
布朗
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.