背景
在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,而应仅在需要它们的更改发生后才放入挂钩。因此,不太可能找到一个在不更改现有类的情况下添加所有新功能的项目 - 除非有人将其作为学术练习(我的直觉说这将非常困难并且生成的代码不会干净)。