开源库中OCP的好例子

时间:2011-08-28 17:26:51

标签: oop solid-principles open-closed-principle

关于stackoverflow的“开放封闭原则”这个主题有很多讨论。然而,似乎通常对该原则的更宽松的解释是普遍的,因此例如Eclipse可以通过插件进行修改。

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

在公共或OS库中是否有任何严格解释OCP的好例子,你可以通过OCP观察一个特征的演变:有一个类Foo,方法是bar(),而且还有一个FooDoingAlsoX和foo2()在下一版本的库中的方法,其中原始类已被扩展,其中原始代码未被修改。

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

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

3 个答案:

答案 0 :(得分: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原则,但您不应将其视为法律,而应将其视为在正确情况下使用的原则。

当你引用罗伯特·C·马丁时,我想你会引用敏捷原则,模式和实践,如果是这样的话,也会在同一章中读到结论,他说的与我上面说的一样。例如,如果您阅读他的书清洁代码,他会对OCP原则给出一个更加渐进的解释,我会说上面的引用有点不幸,因为它可以让人们认为你应该总是把新代码放在新的DLL中:s,JAR :s或libs,当事实是你应该总是考虑上下文。

我认为你应该看看Martins更新的关于OCP的最新白皮书http://objectmentor.com/resources/articles/ocp.pdf(他在后面的书“清洁代码”中也提到了这一点),在那里他从不提到单独的二进制文件,而是他引用到“课程,模块,功能”。我认为这证明了Martin在谈到OCP时不仅意味着二进制扩展,而且还意味着类和函数的扩展,因此二进制扩展并不比我的第一个例子中的类扩展更“严格”。

答案 1 :(得分:1)

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

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

这方面有一些有趣的联系:

答案 2 :(得分:1)

<强>背景

PPP的第100页罗伯特马丁说

  

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

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

  

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

值得记住的是,本书于2003年出版,许多示例都使用C ++,这是一种因编译时间长而臭名昭着的语言(除非头文件依赖性得到很好处理 - Remedy的开发人员在一个演示文稿中提到{{ 3}} 完整的构建只需要大约2分钟。)

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

示例

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

JUnit最近添加的Runner允许测试编写者插入标准JUnit测试自定义行为,这需要一个自定义测试运行器。再一次是库级OCP的一个例子。

相比之下,TestNG不遵循OCP,但包含JUnit特定检查以不同方式执行TestNG和JUnit测试。可以从@Rule annotation方法找到representative line

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

因此,即使严格的TestNG测试运行器在某些方面具有更多功能(例如支持并行运行测试),其他测试框架也不使用它,因为在不修改TestNG的情况下支持其他测试框架是不可扩展的。 (TestNG有一种方法可以使用TestRunner.run()参数插入custom test runners,但是AFAIK每个套件只允许一种类型的运行器。所以在一个项目中不可能使用许多不同的测试框架,与JUnit不同。)

<强>结论

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

要始终牢记的是,遵循OCP是昂贵的,因为它会使系统变得更加复杂。罗伯特·马丁在PPP第105页和本章的结论中谈到了这一点。 OCP应该谨慎应用,仅适用于最可能的变化。你不应该预先放入钩子来跟随OCP,但是你应该只在需要它们的变化发生之后才进入钩子。因此,在不改变现有类的情况下,不太可能找到一个可以添加所有新功能的项目 - 除非有人将其作为学术练习(我的直觉说它会非常困难并且生成的代码不会干净)。 / p>