重构和开放/封闭原则


12

我最近正在阅读一个有关干净代码开发的网站(我不在此处放置链接,因为它不是英语)。

本网站宣传的原则之一是开放式封闭原则:每个软件组件都应开放以进行扩展,而封闭则可以进行修改。例如,当我们实现并测试了一个类时,我们仅应对其进行修改以修复错误或添加新功能(例如,影响现有方法的新方法)。现有功能和实现不应更改。

我通常通过定义接口I和相应的实现类来应用此原理A。当类A变得稳定(实现并经过测试)后,我通常不会对其进行过多修改(可能根本没有修改),即

  1. 如果新的要求到达需要的代码大的变化(如性能,还是一个全新的接口的实现),我写了一个新的实现B,并使用保持A,只要B还没有成熟。当B成熟时,所需要做的就是更改I实例化方式。
  2. 如果新的要求也建议更改接口,那么我将定义一个新的接口I'和一个新的实现A'。所以IA被冻结并保持实施生产系统,只要I'A'不够稳定,以取代他们。

因此,鉴于这些观察,令我感到惊讶的是该网页随后建议使用复杂的重构,“……因为不可能直接以其最终形式编写代码。”

在执行“开放/封闭原则”与建议使用复杂重构作为最佳实践之间是否存在矛盾/冲突?还是这里的想法是,在开发一个类的过程中可以使用复杂的重构A,但是当成功测试了该类后,应该冻结它吗?

Answers:


9

我认为开放式原则是设计目标。如果您最终不得不违反它,那意味着您的初始设计失败了,这肯定是可能的,甚至是可能的。

重构意味着您无需更改功能即可更改设计。您可能正在更改设计,因为它存在问题。可能的问题在于,在对现有代码进行修改时,很难遵循开闭原则,而您正在尝试解决此问题。

您可能正在进行重构,以实现您的下一个功能,而又不会违反OCP。


您当然不应该将任何原则视为设计目标。它们是工具-您并不是要使软件内部完全美观并且理论上正确无误,而是想为客户创造价值。这只是一个准则,仅此而已。
T. Sar

@ T.Sar原则是一个准则,您要努力做到这一点,所以它们着眼于可维护性和可伸缩性。在我看来,这似乎是一个设计目标。我不能像将设计模式或框架视为工具那样将原理视为工具。
图兰斯·科尔多瓦

@TulainsCórdova可维护性,性能,正确性,可扩展性-这些都是目标。开闭原则是对他们的一种手段 -只是其中一种。如果它不适用于开放原则,则不需要推动它,否则会降低项目的实际目标。您不会 “开放式封闭”卖给客户。仅作为指导原则,如果您最终找到一种以更易懂和清晰的方式来做事的方法,那么它就不会比凭经验做的更好。毕竟,准则只是工具。
萨尔

@ T.Sar有很多东西你不能卖给客户...另一方面,我同意你的看法,那就是不得做任何有损项目目标的事情。
图兰斯·科尔多瓦

9

开放-封闭原则更多地指示了软件的设计水平;并非字面上应遵循的原则。这也是一条原则,有助于防止我们意外更改现有接口(您调用的类和方法以及您希望它们如何工作)。

目的是编写高质量的软件。这些特性之一是可扩展性。这意味着添加,删除,更改代码很容易,而这些更改往往被限制为尽可能少的现有类。与更改现有代码相比,添加新代码的风险较小,因此从这方面考虑,Open-Closed是一件好事。但是我们到底在说什么代码?当您可以向类中添加新方法而不需要更改现有方法时,违反OC的罪行要少得多。

OC是分形的。它遍及您设计的各个角落。每个人都认为它仅适用于课程级别。但这同样适用于方法级别或组装级别。

在适当的级别上过于频繁地违反OC可能意味着该重构了。“适当的级别”是一个与总体设计有关的判断调用。

从字面上看,Open-Closed意味着类的数量将激增。您将不必要地创建(大写“ I”)接口。最后,您将获得分散在各个类中的功能,然后您必须编写更多代码以将所有功能连接在一起。在某个时候,您会发现更改原始类会更好。


2
“当您可以向类中添加新方法而不需要更改现有方法时,违反OC的罪行要少得多。”:据我了解,添加新方法完全不违反OC原则(可扩展) 。问题在于改变了实现一个明确定义的接口并因此已经具有明确定义的语义的现有方法(已关闭以进行修改)。原则上,重构不会改变语义,所以我唯一能看到的风险就是在已经稳定且经过良好测试的代码中引入错误。
乔治

1
这是CodeReview答案,它说明了可扩展性。该类设计是可扩展的。相反,添加方法会修改该类。
Radarbob '16

添加新方法违反了LSP,而不是OCP。
图兰斯·科尔多瓦

1
添加新方法不会违反LSP。如果添加方法,您将引入一个接口@TulainsCórdova–
RubberDuck

6

开放式封闭原则似乎是在TDD更为流行之前出现的一种原则。这样做的想法是重构代码是有风险的,因为您可能会破坏某些内容,因此将现有代码保持原样并直接添加就可以了。在没有测试的情况下,这是有道理的。这种方法的缺点是代码萎缩。每次您扩展一个类而不是对其进行重构时,您都会得到一个额外的层。您只是将代码附加在最上面。每次您添加更多代码时,就会增加重复的机会。想像; 我的代码库中有一个我想使用的服务,我发现它没有我想要的东西,所以我创建了一个新类来扩展它并包含我的新功能。后来有另一个开发人员来,也希望使用相同的服务。不幸的是,他们没有 意识到我的扩展版本存在。他们按照原始实现进行编码,但是还需要我编码的功能之一。他们现在不再使用我的版本,而是扩展实现并添加新功能。现在,我们有3个类,一个是原始的,另一个是两个具有某些重复功能的新版本。遵循开放/封闭原则,这种重复将在项目的整个生命周期中继续累积,从而导致不必要的复杂代码库。

有了一个经过良好测试的系统,就不必遭受代码萎缩的困扰,您可以安全地重构代码,使您的设计能够吸收新的需求,而不必不断地使用新代码。这种开发风格称为紧急设计,它会导致代码库能够在其整个生命周期中保持良好的状态,而不是逐渐收集残篇。


1
我既不是开放式原则的支持者,也不是TDD的拥护者(就我不是发明它们的意义而言)。令我惊讶的是,有人同时提出了开放原则和重构以及TDD的使用。这对我来说似乎是矛盾的,因此我试图弄清楚如何将所有这些准则整合到一个一致的过程中。
乔治

“这样做的想法是重构代码是有风险的,因为您可能会破坏某些内容,因此将现有代码保留为原样并简单地添加就可以了。”:实际上,我并不这样看。想法是拥有可以替换或扩展的小型独立设备(从而使软件得以发展),但是一旦每个设备都经过全面测试,则不要触摸它。
乔治

您必须认为该类不仅会在您的代码库中使用。您编写的库可以在其他项目中使用。因此,OCP很重要。另外,不知道扩展类具有他/她需要的功能的新程序员是通信/文档问题,而不是设计问题。
图兰斯·科尔多瓦

@TulainsCórdova在应用程序代码中与此无关。对于库代码,我认为语义版本控制更适合传达重大更改。
opsb

1
具有库代码API稳定性的@TulainsCórdova更为重要,因为无法测试客户端代码。使用应用程序代码,您的测试范围将立即通知您任何损坏。换句话说,应用程序代码可以毫无风险地进行重大更改,而库代码必须通过保持稳定的API并使用例如语义版本控制来
标记

6

用外行的话来说:

A. O / C原则意味着必须通过扩展来实现专业化,而不是通过修改类来满足专业化需求。

B.添加缺少的(不是专用的)功能意味着设计不完整,您必须将其添加到基类中,显然不能违反合同。我认为这并不违反原则。

C.重构不违反该原则。

当设计成熟后,请说,经过一段时间的生产:

  • 这样做的理由应该很少(B点),随着时间的流逝趋向于零。
  • (点C)总是可能的,尽管这种情况很少发生。
  • 所有新功能都应该是专门化的,这意味着必须扩展(继承)类(点A)。

开放/封闭原则被误解了。您的观点A和B完全正确。
gnasher729

1

对我来说,开放式封闭原则是一个准则,而不是一成不变的规则。

关于该原则的开放部分,Java中的最终类和所有构造函数都声明为private的C ++中的类均违反了开放封闭原则的开放部分。最终课程有很好的固体用例(注意:固体,而不是SOLID)。设计可扩展性很重要。但是,这需要大量的预见和努力,并且您总是绕过违反YAGNI的行(您将不需要),并注入了投机性通用性的代码味道。关键软件组件是否应该开放扩展?是。所有?不。这本身就是投机性的概括。

关于封闭部分,当从某些产品的2.0版到2.1版到2.2版到2.3版时,不修改行为是一个好主意。当每个次要发行版破坏了自己的代码标记时,用户确实不喜欢它。但是,在此过程中,人们经常发现2.0版中的初始实现已被根本破坏,或者限制初始设计的外部约束不再适用。您是否愿意忍受它并在3.0版中保持该设计,还是在某些方面使3.0向后兼容?向后兼容性可能是一个巨大的约束。主要的发行边界是可以接受向后兼容的地方。您确实需要注意,这样做可能会使您的用户不高兴。必须有一个很好的理由来说明为什么需要与过去的突破。


0

根据定义,重构是在不更改行为的情况下更改代码结构。因此,在重构时,您无需添加新功能。

您以“打开关闭”原理为例所做的事情听起来不错。该原则是关于使用新功能扩展现有代码。

但是,不要误解这个答案。我并不是说您应该只对大数据进行功能或只对大数据进行重构。编程的最常见方式是做一些功能,而不是立即做一些重构(当然还要结合测试以确保您没有改变任何行为)。复杂的重构并不意味着“大”重构,它意味着应用复杂且经过深思熟虑的重构技术。

关于SOLID原则。它们确实是软件开发的良好指南,但绝不是盲目遵循的宗教规则。有时,添加第二个,第三个和第n个功能后,很多时候您意识到您的初始设计即使尊重Open-Close,也没有遵守其他原则或软件要求。当需要进行更复杂的更改时,设计和软件会不断发展。关键是要尽快发现并意识到这些问题,并尽可能地应用重构技术。

没有完美的设计。没有这样的设计可以并且应该尊重所有现有的原则或模式。那就是编码乌托邦。

希望这个答案对您的困境有所帮助。如有需要,请随时澄清。


1
“因此,在重构时,您不会添加新功能。”:但是我可以在经过测试的软件中引入错误。
Giorgio

“有时,在添加第二,第三和第n个功能之后,很多时候,您意识到您的初始设计即使尊重Open-Close,也没有遵守其他原则或软件要求。”开始编写新的实现,B并在准备好后,用A新的实现替换旧的实现B(这是接口的一种用法)。A的代码可以用作的代码基础B,然后我可以B在其开发过程中对代码进行重构,但是我认为已经测试过的A代码应保持冻结状态。
乔治

@Giorgio重构时可能会引入错误,这就是编写测试(甚至更好地编写TDD)的原因。重构的最安全方法是在知道代码有效时更改代码。您可以通过一组正在通过的测试来了解这一点。更改生产代码后,测试仍必须通过,因此您知道自己没有引入错误。请记住,测试与生产代码一样重要,因此您要对它们应用与生产代码相同的规则,并保持其清洁并定期且频繁地对其进行重构。
Patkos Csaba

@Giorgio如果代码B基于A的演变建立在代码之上A,则在B发布A时应将其删除并且不再使用。以前使用的客户A将在B不知道更改的情况下使用该服务,因为界面I没有更改(这里可能是Liskov替换原理的一点?... SOLID中的L
Patkos Csaba 2012年

是的,这就是我的想法:在您进行有效(经过良好测试)的替换之前,不要丢弃工作代码。
乔治

-1

根据我的理解-如果您向现有类添加新方法,则不会破坏OCP。但是我有点与在类中添加新变量感到困惑。但是,如果您更改现有方法和现有方法中的参数,则肯定会破坏OCP,因为如果我们有意更改方法(当需求更改时),则代码已经过测试并通过,那么这将成为问题。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.