如果我有一个应用程序的完整单元测试套件,我是否仍然必须应用开放/封闭原则 (OCP)?

发布于 2024-08-05 01:45:03 字数 397 浏览 7 评论 0原文

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

...开放/封闭原则规定“软件实体(类、模块、函数等)应该对扩展开放,但对修改关闭”...这在生产环境中尤其有价值,因为生产环境会发生变化源代码可能需要代码审查、单元测试和其他此类程序以使其能够在产品中使用:< em>遵守该原则的代码在扩展时不会改变,因此不需要这样的努力。

那么,如果没有自动化单元测试,OCP 就会很有价值,但如果有的话,OCP 就不一定有价值,我这样理解是否正确?或者维基百科的文章是错误的?

The Wikipedia article on OCP says (emphasis mine):

... the open/closed principle states "software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification"... This is especially valuable in a production environment, where changes to source code may necessitate code reviews, unit tests, and other such procedures to qualify it for use in a product: code obeying the principle doesn't change when it is extended, and therefore needs no such effort.

So, am I reading it correctly that OCP would be valuable if there is no automated unit testing, but not necessarily if there is? Or is the Wikipedia article wrong?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(8

故事↓在人 2024-08-12 01:45:03

单元测试,根据定义!,是关于单元(通常是单个类)内部的行为:为了正确地完成它们,你需要尽力而为将被测单元与其他单元的交互隔离开来(例如通过模拟、依赖注入等)。

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

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

我认为在某些方面最明显的例子是有争议的做法猴子补丁,允许在动态语言中使用,并在某些此类语言从业者社区中流行(不是全部!-)。猴子修补 (MP) 就是在运行时修改对象的行为而不更改其源代码,因此它说明了为什么我认为您不能从源代码更改的角度排他性解释 OCP。

MP 很好地展示了我刚才给出的示例。 A 和 C 的单元测试都可以出色地通过(即使它们都使用真正的 B 类而不是嘲笑它),因为每个单元本身都工作得很好;即使您测试了两者(因此这已经远远超出了 UNIT 测试),但碰巧您在 A 之前测试了 C,一切看起来都很好。但是,比如说,A 通过设置方法 B.foo 来对 B 进行修补,返回 23(A 需要的值)而不是 45(B 有记录地提供的值,C 所依赖的值)。现在破坏了OCP:B应该关闭以进行修改,但是A不遵守该条件并且语言不强制执行它。然后,如果 A 使用(并修改)B,然后轮到 C,C 会在一种从未测试过的条件下运行——其中 B.foo 未记录且令人惊讶地返回 23(而它总是 在所有测试中返回 45...!-)。

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

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

我所看到的“发布后不可修改”广泛应用的一个利基是组件模型的接口,例如 Microsoft 的旧式 COM —— 任何已发布的 COM 接口都不允许更改(因此您最终会得到 IWhateverExIWhatever2IWhateverEx2 等,当对界面进行修复证明有必要时 - 绝不更改原始 IWhatever< /代码>!-)。

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

我想你们团队中的争论不是来自错误修复(有人争论禁止那些吗?-),甚至不是性能优化,而是来自在哪里放置新功能的问题 - 我们应该将新方法 foo 添加到现有类 A 中,或者将 A 扩展为 B 并添加 foo< /code> 仅到 B,以便 A 保持“关闭修改”状态?单元测试本身还没有回答这个问题,因为它们可能不会执行 A 的所有现有用法(当该实体经过测试...),因此您需要更深入地了解 foo 究竟是什么,或者可能在做什么。

如果 foo 只是一个访问器,并且从不修改调用它的 A 实例,那么添加它显然是安全的;如果 foo 可以改变实例的状态,以及从其他现有方法中可以观察到的后续行为,那么您确实遇到了问题。如果您尊重 OCP 并将 foo 放入单独的子类中,那么您的更改将非常安全且常规;如果您希望将 foo 直接放入 A 中,那么那么确实需要进行广泛的代码审查,请轻“成对组件集成”测试探测 A 的所有使用,等等。这不会限制您的架构决策,但它确实明确指出了任一选择所涉及的不同成本,因此您可以适当地计划、估计和确定优先级。

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

Unit tests, by definition!, are about behavior inside a unit (typically a single class): to do them properly, you try your best to isolate the unit under test from its interactions with other units (e.g. via mocking, dependency injection, and so on).

OCP is about behavior across units ("software entities"): if entity A uses entity B, it can extend it but cannot alter it. (I think the emphasis of the wikipedia article exclusively on source code changes is misplaced: the issue applies to all alterations, whether obtained through source code changes or by other runtime means).

If A did alter B in the process of using it, then unrelated entity C which also uses B might be adversely affected later. Proper unit tests would typically NOT catch the breakage in this case, because it's not confined to a unit: it depends on a subtle, specific sequence of interactions among units, whereby A uses B and then C also tries to use B. Integration, regression or acceptance tests MIGHT catch it, but you can never rely on such tests providing perfect coverage of feasible code paths (it's hard enough even in unit tests to provide perfect coverage within one unit/entity!-).

I think that in some ways the sharpest illustration of this is in the controversial practice of monkey patching, permitted in dynamic languages and popular in some communities of practitioners of such languages (not all!-). Monkey patching (MP) is all about modifying an object's behavior at runtime without changing its source code, so it shows why I think you can't explain OCP exclusively in terms of source code changes.

MP exhibits well the example case I just gave. The unit tests for A and C can each pass with flying colors (even if they both use the real class B rather than mocking it) because each unit, per se, does work fine; even if you test BOTH (so that's already way beyond UNIT testing) but it so happens that you test C before A, everything seems fine. But, say, A monkeypatches B by setting a method, B.foo, to return 23 (as A needs) instead of 45 (as B documentedly supplies and C relies on). Now this breaks the OCP: B should be closed for modification, but A doesn't respect that condition and the language doesn't enforce it. Then, if A uses (and modifies) B, and then it's C's turn, C runs under a condition in which it had never been tested -- one where B.foo, undocumentedly and surprisingly, returns 23 (while it always returned 45 throughout all the testing...!-).

The only issue with using MP as the canonical example of OCP violation is that it might engender a false sense of security among users of languages that don't overtly allow MP; in fact, through configuration files and options, databases (where every SQL implementation allows ALTER TABLE and the like;-), remoting, etc, etc, every sufficiently large and complex project must keep an eye out for OCP violations, even were it written in Eiffel or Haskell (and much more so if the allegedly "static" language actually allows programmers to poke whatever they want anywhere into memory as long as they have the proper cast incantations in place, as C and C++ do -- now THAT's the kind of thing you definitely want to catch in code reviews;-).

"Closed for modification" is a design goal -- it doesn't mean you can't modify an entity's source code to fix bugs, if such bugs are found (and then you'll need code reviews, more testing including regression tests for the bugs being fixed, etc, of course).

The one niche where I've seen "unmodifiable after release" applied widely are interfaces for component models such as Microsoft's good old COM -- no published COM interface is ever allowed to change (so you end up with IWhateverEx, IWhatever2, IWhateverEx2, and the like, when fixes to the interface prove necessary -- never changes to the original IWhatever!-).

Even then, the guaranteed immutability only applies to the interfaces -- the implementations behind those interfaces are always allowed to have bug fixes, performance optimization tweaks, and the like ("do it right the first time" just doesn't work in SW development: if you could release software only when 100% certain that it has 0 bugs and the maximum possible and necessary performance on every platform it will ever be used on, you'd never release anything, the competition would eat your lunch, and you'd go bankrupt;-). Again, such bug fixes and optimizations will need code reviews, tests, and so forth, as usual.

I imagine the debate in your team comes not from bug fixes (is anybody arguing for forbidding those?-), or even performance optimizations, but rather from the issue of where to place new features -- should we add new method foo to existing class A, or rather extend A into B and add foo to B only, so that A stays "closed for modification"? Unit tests, per se, don't yet answer this question, because they might not exercise every existing use of A (A might be mocked out to isolate a different entity when that entity gets tested...), so you need to go one layer deeper and see what foo exactly is, or may be, doing.

If foo is just an accessor, and never modifies the instance of A on which it's called, then adding it is clearly safe; if foo can alter the instance's state, and subsequent behavior as observable from other, existing methods, then you do have a problem. If you respect the OCP and put foo in a separate subclass, your change is very safe and routine; if you want the simplicity of putting foo right into A, then you do need extensive code reviews, light "pairwise component integration" tests probing all uses of A, and so forth. This does not constrain your architectural decision, but it does clearly point at the different costs involved in either choice, so you can plan, estimate and prioritize appropriately.

Meyer's dicta and principles are not a Holy Book, but, with a properly critical attitude, they're very worthwhile to study and ponder in the light of your specific, concrete circumstances, so I commend you for so doing in this case!-)

静谧幽蓝 2024-08-12 01:45:03

我认为你对该死的 OCP 读得太多了。我对此的解释是“在修改现有类之前三思而行,该类的行为依赖于许多不受您控制的代码”。

如果唯一的用户是你和你的狗,当然你可以修改类的内容,同时非常高效并且完全没有问题。

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

  • 冒着破坏某些东西的风险,
  • 让他们扩展它,
  • 通过自己扩展它来使设计变得臃肿,

选择最适合您的用例的选项。

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

I think you are reading too much into the damned OCP. My interpretation of it is "think thrice before modifying an existing class on whose behavior depend lots of code which is not controlled by you".

If the only users are you and your dog, of course you can modify the guts out of the class while being very efficient and have no problems at all.

If your users (regardless of their being internal or external) are many, you really have to consider all the impact a change inside a working class can have, and, if your user base is huge, you just can't anticipate and will have to:

  • risk breaking something for somebody
  • let them extend it
  • bloat the design by extending it yourself

pick the best for your use case.

As always, understanding the context and the tradeoffs is what makes engineering interesting. Know when to pick the proper tool. There are times when OCP does not apply, but that doesn't ivalidate its helpfulness, if you considered it and rejected it because it doesn't apply to your context for A and B.

娇妻 2024-08-12 01:45:03

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

维基百科文章,IMO,假设总是使用高质量的流程,例如单元测试和代码审查(在 XP 中这转化为 TDD 和结对编程),即使在使用 OCP。接下来要说的是,通过 OCP,您可以更好地控制更改的范围,从而减少这些质量流程中的工作量。

Good design principles (like the OCP) are not add odds with good development processes (like unit tests and TDD). They're complementary.

The Wikipedia article, IMO, assumes that you're always using good quality processes like unit tests and code reviews (in XP this translates to TDD and pair programming), even when using the OCP. What it goes on to say is that, with the OCP, you better control the scope of your changes, which results in less effort in these quality processes.

桃气十足 2024-08-12 01:45:03

我认为即使你有自动化的单元测试,这仍然是一个有价值的原则。

I think it is still a valuable principle even if you have automated unit testing.

水波映月 2024-08-12 01:45:03

对一段代码进行更改也可能会破坏测试,例如更改方法的名称。这就是 OCP 的用途 - 不要在需要编辑以前的代码以使其表现不同的情况下创建代码。相反,如果您需要它以不同的方式运行,则可以通过扩展来实现。

你可以在很多地方看到这种设计:(N)Hibernate Interceptors、WPF 数据模板等等。:)

Making changes to a piece of code might also break the tests, by for example changing the name of a method. This is what the OCP is for - Don't create code where you need to edit previous code in order to make it behave differently. Instead, make it so that if you need it to act differently, you can do so by making extensions.

You can see this kind of design a lot of places: (N)Hibernate Interceptors, WPF data templates, etc. etc.. :)

羅雙樹 2024-08-12 01:45:03

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

根据该文章中的一些评论,以及 this学术论文(B.2节),回答“有了单元测试,还必须应用OCP吗?”似乎是

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

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

This article from the C2 Wiki discusses the tension between OCP and XP.

From some comments in that article, and from this academic dissertation (section B.2), the answer to the question of "with unit tests, must OCP still be applied?" would appear to be no.

The reasoning is that OCP addresses the impact of functional changes to working code through up-front design, where abstractions are created early in the hope that they will provide enough extensibility later when new requirements come in.

Agile development (with XP or just TDD), on the other hand, addresses the impact of change through evolutive design ("Embrace change", anyone?), not by attempting to devise abstractions up-front. And as we all know, up-front design hardly works in practice.

囚你心 2024-08-12 01:45:03

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

Like most ideas originated by Bertrand Meyer, the Open/Closed Principle is pretty much just flat-out wrong. If your system needs some new functionality, and that functionality belongs in an existing class, change that class. Don't put it somewhere else just to satisfy an arbitrary law.

爱的故事 2024-08-12 01:45:03

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

Unit tests help in upholding the open/closed principle: the necessary changes (refactoring of bad code) are validated by a unit test suite to check that no externally visible behavior changes have occurred.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文