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

在React的快速和连续版本之后,这个问题跳到我的头上库。

在很短的时间内,我会注意到语法,组件名称,... etc的许多变化

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


新的弃用警告

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

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




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


评论

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

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

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

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

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

#1 楼

IMHO JacquesB的回答虽然包含很多事实,但显示出对OCP的根本误解。公平地说,您的问题也已经表达了这种误解-重命名功能破坏了向后兼容性,但没有破坏OCP。如果似乎需要打破兼容性(或维护同一组件的两个版本以不破坏兼容性),那么OCP之前就已经被破坏了! “您不能修改组件的行为”-它说,应该尝试以一种开放的方式设计组件,以便以多种方式重复使用(或扩展)蜜蜂,而无需进行修改。这可以通过提供正确的“扩展点”来完成,或者如@AntP所述,“通过将类/函数结构分解为默认情况下每个自然扩展点都存在的点”来完成。遵循OCP的恕我直言与“保持旧版本保持不变以向后兼容”没有任何共同点!或者,在下面引用@DerekElkin的评论:OCP是有关如何编写模块的建议,而不是有关实现永远不允许模块进行更改的更改管理过程的建议。


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

评论


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

– BJ Myers
17年4月30日在21:39

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

–布朗博士
17年4月30日在21:42

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

–candied_orange
17年4月30日在23:04

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

–布朗博士
17年5月1日,0:17



这个答案很好地指出了(当前)最佳答案中的错误-我认为,尽管成功完成了打开/关闭操作,关键是要停止思考“扩展点”,而开始考虑分解您的类/函数结构到默认情况下每个自然扩展点都存在的位置。进行“由外而内”编程是实现此目标的一种非常好的方法,其中,您当前的方法/函数所满足的每种情况都将被推出到外部接口,从而形成装饰器,适配器等的自然扩展点。

– Ant P
17年5月1日在11:24

#2 楼

开放/封闭原则有好处,但也有一些严重的缺点。如果一个类有一些新要求,则您永远不要修改该类本身的源代码,而是创建一个子类,该子类仅覆盖更改行为所需的适当成员。因此,针对该类原始版本编写的所有代码均不受影响,因此您可以确信所做的更改不会破坏现有代码。
实际上,您很容易陷入代码膨胀和混乱的过时类混乱之中。如果无法通过扩展修改组件的某些行为,则必须提供具有所需行为的组件的新变体,并保持旧版本不变以实现向后兼容性。
如果发现一个基本类的基本设计缺陷,许多类都可以从中继承。说该错误是由于私有字段的类型错误引起的。您无法通过覆盖成员来解决此问题。基本上,您必须重写整个类,这意味着您最终需要扩展Object以提供替代的基类-现在您还必须提供所有子类的替代方法,从而最终导致重复的对象层次结构,一个层次结构存在缺陷,一个层次结构改善。但是您不能删除有缺陷的层次结构(因为修改代码是修改代码),所有未来的客户端都将同时处于这两个层次结构中。如果代码被完美地分解,没有任何缺陷或错误,并且设计有为将来所有可能的需求变更准备的扩展点,那么您可以避免混乱。但是实际上每个人都会犯错,没有人能完美预测未来。
像.NET框架这样的东西-它仍然包含了集合类的集合,这些集合类是在十多年前引入泛型之前设计的。这无疑是向后兼容性的福音(您可以升级框架而无需重写任何内容),但是它也使框架膨胀,并为开发人员提供了很多选择,而其中许多选择已经过时了。觉得严格遵循开放/封闭原则并不值得付出复杂性和繁琐的代码。
开放/封闭的实用替代方法是控制弃用。不会在单个发行版本中破坏向后兼容性,而是将旧组件保留一个发行周期,但是会通过编译器警告通知客户端,该旧方法将在以后的发行版中删除。这使客户有时间修改代码。在这种情况下,这似乎是React的方法。
(我对原理的解释是基于Robert C. Martin的“开放-闭合原理”)

评论


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

–Jörg W Mittag
17年4月30日在17:39

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

–Jörg W Mittag
17年4月30日在17:40

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

–Jörg W Mittag
17年4月30日在18:00

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

–Jörg W Mittag
17年4月30日在18:03

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

–Jörg W Mittag
17年5月1日在9:40

#3 楼

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

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

Windows 95内存管理器中就是一个著名的例子。作为Windows 95营销的一部分,据说所有Windows 3.1应用程序都可以在Windows 95中运行。微软实际上获得了数千个程序的许可以在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
17年4月30日在21:26

@BJMyers这是一个古老的故事,Joel Spoleky在本文结尾处提到了它。几年前,我最初将其作为有关开发视频游戏的书的一部分进行阅读。

–Cort Ammon
17年4月30日在21:28

@BJMyers:我很确定他们对数十种流行应用程序都具有类似的兼容性“ hacks”。

–布朗博士
17年4月30日在21:33

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

– Theraot
17年5月1日在6:12



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

–罗安
17年5月1日在17:15

#4 楼

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

为了明确表达这种误解,似乎有人认为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查找各种其他文章,有些是讽刺性的,从多个角度讨论这个问题。)

评论


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

–雅克B
17年5月1日在11:31

@JacquesB人员甚至代码更改都没有违反OCP,而组件(即实际的代码段)确实违反了OCP。 (而且,非常清楚地表示,这意味着该组件无法达到OCP本身的要求,而不是它违反了其他某些组件的OCP。)我的回答的全部重点是,OCP并不是在谈论代码更改。 ,损坏或其他原因。组件可以扩展,而不能修改,就像方法可以是私有的一样,也可以不是。如果作者稍后将私有方法公开,则并不意味着他们已经违反了访问控制,(1/2)

–德里克·埃尔金斯离开东南
17年5月1日在12:26

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

–德里克·埃尔金斯离开东南
17年5月1日在12:26

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

–德里克·埃尔金斯离开东南
17年5月1日14:51

“ OCP是特定代码段的属性,而不是代码库的演进历史。” - 优秀的!

–布朗博士
17年5月2日在6:33