6

关于stackoverflow上的“开闭原则”这个话题已经有很多讨论了。然而,一般来说,对原理的更宽松的解释似乎很普遍,因此例如 Eclipse 是开放的,可以通过插件进行修改。

根据严格的 OCP,您应该只修改原始代码以修复错误,而不是添加新行为。

在公共或 OS 库中是否有任何严格解释 OCP 的好例子,您可以在其中通过 OCP 观察功能的演变:有一个带有方法 bar() 的类 Foo,然后有一个带有 foo2() 方法的 FooDoingAlsoX库的下一个版本,其中原始类已在未修改原始代码的情况下进行了扩展。

编辑:根据 Robert C. Martin 的说法:“模块的二进制可执行版本,无论是可链接库、DLL 还是 Java .jar 都保持不变”*。我从来没有看到图书馆保持关闭,实际上新行为被添加到图书馆并发布了新版本。根据 OCP,新行为属于新的二进制模块。

*Robert C. Martin 的敏捷软件开发、原则、模式和实践

4

3 回答 3

3

OCP 原则说,一个类应该对扩展开放但对更改关闭。实现这一点的关键是抽象。如果您还阅读了 DIP 原则,您会发现抽象不应该依赖于细节,而细节应该依赖于抽象。在您的示例中,您的界面中有详细信息(两个特定方法 bar() 和 foo2())。要完全实现 OCP,您应尽量避免此类细节(例如,尝试将它们移到抽象后面,而是使用一种具有不同实现的通用 foo 方法)。

例如看一下 SolrNet 中的这个接口: https ://github.com/mausch/SolrNet/blob/master/SolrNet/ISolrCommand.cs 这是一个通用命令,它只告诉一个命令可以执行,它没有'不要提供比这更多的细节。

相反,细节在于接口的实现: https ://github.com/mausch/SolrNet/tree/master/SolrNet/Commands

如您所见,您可以根据需要添加任意数量的命令,而无需更改任何其他类的实现。特定的实现可以被认为是封闭的修改,但接口允许我们用新的命令来扩展功能,因此是开放的。

(无论如何,SolrNet 并不特别,我只是使用了这个项目中的示例,因为我在阅读这篇文章时碰巧在我的浏览器中拥有它,几乎所有好的编码 OO 项目都以一种或另一种方式利用了 OCP 原则)

编辑:如果您想要二进制级别的示例,例如可以查看 nopCommerce (http://nopcommerce.codeplex.com/releases/view/69081),例如,您可以在其中添加自己的运输提供商、付款通过实现一组接口,甚至无需触及原始 DLL。再说一次,这并不是 nopCommerce 的特别之处,它只是我想到的第一个项目,因为我几天前使用了它;)

OCP 不是一个只能在二进制级别使用的原则,好的 OOD 使用 OCP,不是在所有地方,而是在所有适合的级别;)二进制级别的“严格”OCP 并不总是合适的,并且会添加一个如果您在每种情况下都使用它,那么复杂度会更高,当您希望在运行时更改实现或希望让外部开发人员能够扩展您的接口时,它最有趣。在设计接口时,应始终牢记 OCP 原则,但不应将其视为法律,而是应在正确情况下使用的原则。

我猜你在引用 Robert C Martin 的时候指的是敏捷原则、模式和实践,如果是这样,也请阅读同一章中的结论,他说的与我上面所做的相同。例如,如果您阅读他的书 Clean Code,他对 OCP 原理给出了更深入的解释,我会说上面的引用有点不幸,因为它可以让人们认为您应该总是将新代码放入新的 DLL:s、JAR :s 或 libs 当事实是你应该始终考虑上下文时。

我认为你宁愿看看 Martins 关于 OCP 的最新白皮书http://objectmentor.com/resources/articles/ocp.pdf(他在他后来的书清洁代码中也提到过),他从来没有提到分开二进制文件,而不是他指的是“类、模块、函数”。我认为这证明了 Martin 在谈到 OCP 时不仅仅是指二进制扩展,还包括类和函数的扩展,因此二进制扩展并不比我第一个示例中的类扩展更“严格”。

于 2011-09-05T12:55:11.940 回答
1

我不知道真正好的例子,但我认为可能有更“宽松的解释”的原因(例如这里的 SO):

要在现实世界的项目中完全实现 OCP 原则,您需要通过精益接口(参见 ISP 和 DIP)和依赖注入(基于属性或构造函数)进行耦合......否则你真的很快要么卡住要么需要诉诸“宽松的解释”……

这方面的一些有趣的链接:

于 2011-08-31T01:57:45.853 回答
1

背景

PPP的第 100 页,罗伯特·马丁说

“关闭以供修改”
扩展模块的行为不会导致模块的源代码或二进制代码发生更改。模块的二进制可执行版本,无论是可链接库、DLL 还是 Java .jar,都保持不变。

同样在第 103 页,他讨论了一个用 C 编写的示例,其中非 OCP 设计导致重新编译现有类:

因此,我们不仅必须更改所有 witch/case 语句或 if/else 链的源代码,而且还必须更改使用任何 Shape 数据结构的所有模块的二进制文件(通过重新编译)。更改二进制文件意味着必须重新部署任何 DLL、共享库或其他类型的二进制组件。

很高兴记住这本书是在 2003 年出版的,许多示例都使用 C++,这是一种因编译时间长而臭名昭著的语言(除非头文件依赖关系处理得很好——Remedy 的开发人员在一次演示中提到Alan Wake完整构建只需大约 2 分钟)。

因此,当讨论小规模(即在一个项目中)的二进制兼容性时,OCP(和 DIP)的一个好处是编译时间更快,这对于现代语言和机器来说不是问题。但是在大规模情况下,当一个库被许多其他项目使用时,特别是如果他们的代码不在我们的控制范围内,不必发布新版本软件的好处仍然适用。

例子

作为在二进制兼容性方面遵循 OCP 的开源库的示例,请查看 JUnit。有数十个测试框架依赖于 JUnit 的@RunWith注解和Runner接口,因此它们可以与 JUnit 测试运行器一起运行 - 无需更改 JUnit、Maven、IDE 等。

此外,JUnit 最近添加的@Rule 注释允许测试编写者插入标准 JUnit 测试自定义行为,这在以前需要自定义测试运行程序。又是一个库级 OCP 的示例。

相比之下,TestNG 不遵循 OCP,但包含特定于 JUnit 的检查以不同地执行 TestNG 和 JUnit 测试。可以从TestRunner.run()方法中找到代表行:

  if(test.isJUnit()) {
    privateRunJUnit(test);
  }
  else {
    privateRun(test);
  }

因此,即使是坚韧的TestNG测试运行器在某些方面具有更多的特性(例如支持并行运行测试),其他测试框架不使用它,因为不修改TestNG就无法扩展支持其他测试框架。(TestNG 有一种方法可以使用-testrunfactory参数插入自定义测试运行器,但 AFAIK 它只允许每个套件使用一种类型的运行器。因此,与 JUnit 不同,不可能在一个项目中使用许多不同的测试框架。)

结论

然而,在大多数情况下,OCP 用于应用程序或库中,在这种情况下,基本模块及其扩展都打包在同一个二进制文件中。在这种情况下,OCP 用于提高源代码的可维护性,而不是避免重新部署和新版本。不必重新编译未更改文件的可能好处仍然存在,但由于大多数现代语言的编译时间如此之短,所以这不是很重要。

要始终牢记的是,遵循 OCP 是昂贵的,因为它使系统更加复杂。Robert Martin 在 PPP 第 105 页和本章的结尾谈到了这一点。应谨慎应用 OCP,仅针对最可能发生的变化。您不应该抢先加入挂钩以遵循 OCP,而应仅在需要它们的更改发生后才放入挂钩。因此,不太可能找到一个在不更改现有类的情况下添加所有新功能的项目 - 除非有人将其作为学术练习(我的直觉说这将非常困难并且生成的代码不会干净)。

于 2011-09-05T22:20:41.377 回答