阐明开放/封闭原则


25

正如我已经解释的那样,开放/封闭原则指出,一旦编写代码,就不应修改(除了错误修复)。但是,如果我的业务规则发生更改,我是否应该修改实现这些更改的代码?我怀疑我不了解该原理,因为这对我来说没有意义。

Answers:


22

这可能是最难解释的可靠原则。让我试试。想象一下,您编写了一个Invoice类,该类可以完美运行并且没有错误。生成发票PDF。

然后有人说他们想要带有链接的HTML发票。您无需更改发票中的任何代码即可满足此要求。相反,您可以创建另一个类,即HTMLInvoice,该类可以完成他们现在想要的。利用继承,您不必在HTMLInvoice中编写很多重复的代码。

使用旧发票的旧代码不会被破坏或以任何方式受到真正影响。新代码可以使用HTMLInvoice。(如果您也执行Liskov Substitutability,即L,您可以将HTMLInvoice实例提供给期望有Invoice实例的现有代码。)每个人都过着幸福的生活。

发票不可修改,可扩展。而且,您必须提前正确编写发票才能使用,顺便说一句。


1
如果业务规则发生变化,那么就不会有没有错误的完美工作假设,那么开放/关闭原则不适用吗?
JeffO 2010年

我自己为这条规则而苦苦挣扎,而凯特的建议基本上就是我对此得出的结论。在业务中,您尝试编写更小,更灵活的类,以便可以很好地重用它们。如果您使它们正常工作,则您不想对其进行修改。但是它们很少完全“完成”,因此不可避免地需要进行一些修改。请注意,虽然文本是“模块”,但不是对象。我经常在功能级别成功应用OCP,而紧密的功能可以完美地完成一件事情,而无需更改。
CodexArcanum 2010年

1
@Jeff OI区分修复错误(代码不符合原始要求并且没有人按原样要求)和更改要求。如果我需要的PDF文件和代码使PDF文件,有没有错误,尽管我现在想的HTML(通常人们想的HTML以及的,而不是替代。)
凯特·格雷戈里

2
@Winston-当我说您必须正确编写发票时,这就是我的意思。理想情况下,已经有了一个漂亮的抽象发票,并且您希望继承此发票。如果没有,则必须打破规则一次,以使自己将来不再违反该规则。无论哪种方式,预测未来的变化都是这一切的重要组成部分,而这就是配方中“捉住大象”的部分。
凯特·格雷戈里

1
您的最后声明是最重要的。打开/关闭是一种理想的设计-您必须提前完成设计才能实现。也不是所有事物都需要满足打开/关闭的要求,但是如果您能做到这一点,它就是一个强大的工具。
Alex Feinman 2013年

13

您是否阅读过ObjectMentor的Bob叔叔的朋友们的“开放原则”文章?我认为这是最好的解释之一。

有许多与面向对象设计相关的试探法。例如,“所有成员变量应为私有”,或“应避免使用全局变量”,或“使用运行时类型标识(RTTI)是危险的”。这些启发式的来源是什么?是什么使它们真实?他们总是对的吗?本专栏将探讨作为这些启发式方法基础的设计原理-开闭原理。

正如Ivar Jacobson所说:“所有系统在生命周期中都会发生变化。在开发预期比第一个版本更长时间使用的系统时,必须牢记这一点。早在1988年,贝特兰德·迈耶(Bertrand Meyer)就提出了当今著名的开闭原理,为我们提供了指导。释述他:

应该打开软件实体(类,模块,功能等),但要对其进行修改则关闭。

当对程序的单个更改导致对从属模块的级联更改时,该程序将显示出我们与“不良”设计相关联的不良属性。该程序变得脆弱,僵化,不可预测和不可重用。开闭原理以非常直接的方式对此进行了攻击。它说您应该设计永远不变的模块。当需求改变时,您可以通过添加新代码而不是通过更改已经起作用的旧代码来扩展此类模块的行为。

描述

符合开闭原理的模块具有两个主要属性。

  1. 它们是“开放扩展”。
    这意味着可以扩展模块的行为。我们可以使模块随着应用程序需求的变化或满足新应用程序的需求而以新的和不同的方式运行。
  2. 它们是“封闭修改”。
    这样的模块的源代码不正确。不允许任何人对其进行源代码更改。

似乎这两个属性相互矛盾。扩展模块行为的正常方法是对模块进行更改。通常认为无法更改的模块具有固定的行为。如何解决这两个相反的属性?

抽象是关键...


3
这是一篇很好的文章,解释了抽象。但是,有一个基本要考虑的问题,那就是首先要布局好的抽象设计吗?许多商店都有很多旧代码,更改它的唯一方法是“修改”,而不是“扩展”。如果是这种情况,那么可能应该可以更改它,但是在此之前,您将不得不修改代码。
Michael K

@Chris,很酷-如果您喜欢这种事情,我也推荐Bob叔叔的“清洁代码”书。
Martijn Verburg

@Michael-完全同意,这就像必须重构代码以使其可测试是理想的。
Martijn Verburg

本文很好地演示了抽象的重要性。但是我没有掌握抽象之间的联系,并且在编写模块之后再也不想修改模块。这种抽象意味着我可以在不对模块Y进行任何修改的情况下修改模块X。但这不是要这样做的意义,因此我可以根据需要修改模块X或模块Y?
温斯顿·埃韦特

1
哇。代码是否违反规定?我从来都不是鲍勃叔叔的忠实粉丝。这个原则是古怪的,极其不务实的,与现实的联系有限。
user949300

12

凯特·格雷戈里(Kate Gregory)的回答非常好,但考虑另一种情况,即通过对现有Invoice班级进行较小的更改就可以满足新的要求。例如,假设必须在发票PDF中添加一个新字段。根据OCP,即使可以通过更改几行代码在现有实现中添加新字段,我们仍然应该创建一个新的子类。

以我的理解,OCP反映了80年代和90年代初的现实,那里的项目通常甚至不使用版本控制,更不用说进行自动回归测试或使用复杂的重构工具了。OCP试图避免破坏手动测试并投入生产的代码的风险。如今,我们有更好的方法来管理破坏可用软件(即版本控制系统,TDD和自动化测试以及重构工具)的风险。


2
是的,因为在实践中,除非创建所有受保护的方法(这很糟,而且还违反了比O / C重要得多的YAGNI原则),否则不可能创建一个可以扩展以适合所有可能的期货的类。 dito)。
Martin Wickman 2013年

“根据OCP,即使可以通过更改几行代码来在现有实现中添加新字段,我们仍然应该创建一个新的子类。”:真的吗?为什么不添加新字段或新方法?重要的一点是,您仅添加(扩展)而不更改已存在的内容。
乔治(Giorgio)

我认为该原则在处理标准库/框架时很有意义。您不想打开和修改完善的代码段。否则,所有这些都与持续重构和测试,测试,测试有关。
mastaBlasta 2014年

@Giorgio当然,增加新的字段或方法什么,我会建议,在大多数情况下。但这不是扩展,而是“修改”。OCP的全部要点是,在“打开扩展名”的同时,代码应“关闭以进行修改”(即,对现有的源文件不做任何更改);OCP中的扩展是通过实现继承来实现的。
罗杰里奥2014年

@Rogério:为什么要在类级别定义扩展和修改之间的边界?是否有特定原因?我宁愿在方法级别进行设置:更改方法会更改应用程序的行为,添加(公共)方法会扩展其接口。
乔治

6

我个人认为该原则应适量食用。代码是有机的,随着时间的流逝,业务会根据业务需求进行更改和更改。

我发现很难理解抽象是关键这一事实。如果抽象本来是错误的怎么办?如果业务功能发生了重大变化怎么办?

该原则从根本上确保了设计的原始意图和行为绝不能改变。这可能适用于那些拥有公共API且其客户难以跟上新版本和其他一些极端情况的人。但是,如果公司拥有所有代码,那么我将挑战这一原则。

对代码进行良好的测试覆盖应该使重构代码基础变得轻而易举。这意味着可以将事情弄错了-您的测试将有助于指导您进行更好的设计。

这样说,如果没有任何测试,那么这个原理是合理的。

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.