-2

关于 OCP 的维基百科文章说(强调我的):

... 开放/封闭原则指出“软件实体(类、模块、函数等)应该对扩展开放,但对修改关闭”... 这在生产环境中特别有价值,其中源代码的更改可能需要代码审查、单元测试和其他此类程序来使其在产品中使用的资格遵循原则的代码在扩展时不会改变,因此不需要这样的努力

那么,如果没有自动化单元测试,我是否正确地阅读了 OCP 将是有价值的,但如果有不一定?还是维基百科的文章错了?

4

8 回答 8

5

单元测试,顾名思义!是关于单元内部的行为(通常是单个类):要正确执行它们,您会尽力将被测单元与其与其他单元的交互隔离开来(例如,通过模拟、依赖注入和很快)。

OCP 是关于单元(“软件实体”)的行为:如果实体 A 使用实体 B,它可以扩展它但不能改变它。(我认为维基百科文章只强调源代码更改是错误的:这个问题适用于所有更改,无论是通过源代码更改还是通过其他运行时方式获得)。

如果 A 在使用它的过程中确实改变了 B,那么同样使用 B 的不相关实体 C 可能会在以后受到不利影响。在这种情况下,适当的单元测试通常不会发现问题,因为它并不局限于一个单元:它取决于单元之间微妙的、特定的交互顺序,其中 A 使用 B,然后C 也尝试使用 B。 集成,回归或验收测试可能会抓住它,但是您永远不能依赖此类测试来提供对可行代码路径的完美覆盖(即使在单元测试中也很难在一个单元/实体中提供完美覆盖!-)。

我认为在某些方面最能说明这一点的是猴子修补的有争议的做法,在动态语言中是允许的,并且在这种语言的一些从业者社区中很流行(不是全部!-)。Monkey patching (MP) 就是在不更改源代码的情况下在运行时修改对象的行为,因此它说明了为什么我认为您不能根据源代码更改来解释 OCP。

MP很好地展示了我刚刚给出的示例。A 和 C 的单元测试都可以顺利通过(即使它们都使用真正的 B 类而不是模拟它),因为每个单元本身都可以正常工作;即使你同时测试了两者(所以这已经超出了单元测试)但是碰巧你在 A 之前测试了 C,一切看起来都很好。但是,比如说,A 通过设置方法 B.foo 来为 B 打补丁,以返回 23(根据 A 的需要)而不是 45(根据 B 提供的文件和 C 的依赖)。现在打破了 OCP:B 应该被关闭以进行修改,但 A 不尊重该条件并且该语言不强制执行它。然后,如果 A 使用(并修改)B,然后轮到 C,C 在从未经过测试的条件下运行——B.foo,无证且令人惊讶地,在整个测试过程中总是返回 45...!-)。

使用 MP 作为违反 OCP 的典型示例的唯一问题是,它可能会在不公开允许 MP 的语言的用户中产生错误的安全感。事实上,通过配置文件和选项、数据库(每个 SQL 实现允许ALTER TABLE的地方等等;-)、远程处理等,每个足够大和复杂的项目都必须留意 OCP 违规,即使它是用 Eiffel 编写的或 Haskell(如果所谓的“静态”语言实际上允许程序员将任何他们想要的东西放入内存中,只要他们有适当的强制转换咒语,就像 C 和 C++ 所做的那样——那就更是如此了——现在就是这样的事情你肯定想赶上代码审查;-)。

“关闭以供修改”是一个设计目标——这并不意味着您不能修改实体的源代码来修复错误,如果发现此类错误(然后您将需要代码审查,包括回归测试在内的更多测试)当然,正在修复的错误等)。

我看到“发布后不可修改”被广泛应用的一个利基是组件模型的接口,例如微软的旧 COM —— 不允许更改已发布的 COM 接口(所以你最终会得到IWhateverEx, IWhatever2,IWhateverEx2等,当证明有必要对界面进行修复时-永远不要更改原始版本IWhatever!-)。

即使这样,保证的不变性也只适用于接口——这些接口背后的实现总是被允许进行错误修复、性能优化调整等(“第一次就做对”在软件开发中是行不通的) : 如果你只能在 100% 确定它有 0 个错误并且在每个将要使用的平台上具有最大可能和必要性能的情况下才能发布软件,那么你永远不会发布任何东西,竞争对手会吃掉你的午餐,而你d 破产;-)。同样,像往常一样,此类错误修复和优化将需要代码审查、测试等。

我想你团队中的争论不是来自错误修复(是否有人争论禁止那些?-),甚至是性能优化,而是来自于在哪里放置新功能的问题——我们是否应该foo向现有类添加新方法A,或者更确切地说只扩展AB添加,以便foo保持“关闭以进行修改”?单元测试本身还没有回答这个问题,因为它们可能不会行使所有现有的使用(可能会在测试实体时被模拟以隔离不同的实体......),所以你需要一层更深入地了解正在可能正在做什么。BAAAfoo

如果foo只是一个访问器,并且从不修改A调用它的实例,那么添加它显然是安全的;如果foo可以改变实例的状态,以及从其他现有方法可以观察到的后续行为,那么你确实有问题。如果您尊重 OCP 并放入foo单独的子类,那么您的更改是非常安全和常规的;如果您想要简单foo放入.AA,等等。这不会限制您的架构决策,但它确实明确指出了两种选择所涉及的不同成本,因此您可以适当地计划、估计和确定优先级。

迈耶的格言和原则不是一本圣书,但是,以适当的批判态度,根据您的具体情况,非常值得研究和思考,因此我赞扬您在这种情况下这样做!-)

于 2009-09-13T16:15:29.393 回答
3

我认为您对该死的 OCP 了解得太多了。我对它的解释是“在修改现有类之前三思而后行,其行为取决于大量不受您控制的代码”。

如果唯一的用户是您和您的狗,那么您当然可以在非常高效且完全没有问题的情况下修改胆量。

如果您的用户(无论是内部用户还是外部用户)很多,那么您确实必须考虑工人阶级内部的变化可能产生的所有影响,并且,如果您的用户群很大,您就无法预料并且将会有至:

  • 冒着为某人破坏某物的风险
  • 让他们扩展它
  • 通过自己扩展设计来膨胀设计

选择最适合您的用例。

与往常一样,了解上下文和权衡是使工程变得有趣的原因。知道何时选择合适的工具。有时 OCP 不适用,但这并不能证明它的有用性,如果您考虑它并拒绝它,因为它不适用于 A 和 B 的上下文。

于 2009-09-13T15:03:22.610 回答
1

良好的设计原则(如 OCP)不会增加良好的开发过程(如单元测试和 TDD)的几率。它们是互补的。

Wikipedia 文章 IMO假设始终使用高质量的流程,例如单元测试和代码审查(在 XP 中这转化为 TDD 和结对编程),即使在使用 OCP 时也是如此。它继续说的是,使用 OCP,您可以更好地控制更改的范围,从而减少这些质量流程的工作量。

于 2010-10-08T16:01:41.627 回答
-1

我认为即使您进行了自动化单元测试,它仍然是一个有价值的原则。

于 2009-09-13T14:52:06.223 回答
-1

更改一段代码也可能会破坏测试,例如更改方法的名称。这就是 OCP 的用途 - 不要在需要编辑以前代码以使其行为不同的地方创建代码。相反,如果你需要它以不同的方式行动,你可以通过扩展来做到这一点。

你可以在很多地方看到这种设计:(N)Hibernate Interceptors, WPF data templates, etc. etc. :)

于 2009-09-13T14:55:38.523 回答
-1

这篇来自 C2 Wiki 的文章讨论了 OCP 和 XP 之间的紧张关系。

从那篇文章的一些评论和这篇学术论文(B.2 节)中,可以回答“通过单元测试,是否还必须应用 OCP?”这个问题的答案。似乎没有

原因是 OCP 通过前期设计解决了功能更改对工作代码的影响,其中提早创建抽象是希望它们在以后有新需求时提供足够的可扩展性。

另一方面,敏捷开发(使用 XP 或仅 TDD)通过演进式设计(“拥抱变化”,有人吗?),而不是通过尝试预先设计抽象来解决变化的影响。众所周知,前期设计在实践中很难奏效。

于 2009-09-13T21:01:12.833 回答
-2

与 Bertrand Meyer 提出的大多数想法一样,开放/封闭原则几乎完全是错误的。如果您的系统需要一些新功能,并且该功能属于现有类,请更改该类。不要为了满足任意法律而将其放在其他地方。

于 2009-09-13T14:53:44.817 回答
-2

单元测试有助于维护开放/封闭原则:必要的更改(错误代码的重构)由单元测试套件验证,以检查没有发生外部可见的行为更改。

于 2009-09-13T14:57:34.280 回答