您是否利用了封闭式原则的好处?


12

开闭原理(OCP)指出对象应打开以进行扩展,但应关闭以进行修改。我相信我理解它,并将其与SRP结合使用来创建仅做一件事的类。而且,我尝试创建许多小的方法,使所有行为控件都可以提取到可以在某些子类中扩展或重写的方法中。因此,我最终得到了具有许多扩展点的类,包括:依赖项注入和组合,事件,委托等。

考虑以下简单的可扩展类:

class PaycheckCalculator {
    // ...
    protected decimal GetOvertimeFactor() { return 2.0M; }
}

现在说,例如,OvertimeFactor更改为1.5。由于上述类是为扩展而设计的,因此我可以轻松地子类化并返回一个不同的OvertimeFactor

但是 ...尽管该类是为扩展而设计的,并且坚持使用OCP,但我将修改单个方法,而不是子类化和覆盖该方法,然后在IoC容器中重新连接对象。

结果,我违反了OCP尝试完成的部分工作。感觉就像是在偷懒,因为上面的内容要容易一些。我是否误解了OCP?我真的应该做些不同的事情吗?您是否以其他方式利用OCP的优势?

更新:根据答案,由于许多不同的原因,这个人为的例子似乎是一个糟糕的例子。该示例的主要目的是证明该类是通过提供以下方法来扩展的:该方法在被重写时将更改公共方法的行为,而无需更改内部或私有代码。不过,我绝对误会了OCP。

Answers:


10

如果您要修改基类,那么它不是真的关闭了!

想想您将图书馆发布给全世界的情况。如果您通过将加班系数修改为1.5来更改基类的行为,那么您将违反所有使用您的代码的人员,并假定该类已关闭。

确实要使类关闭但打开,您应该从替代源(可能是配置文件)中获取超时因素,还是证明可以重写的虚拟方法?

如果确实关闭了该类,那么在您进行更改之后,任何测试用例都不会失败(假设您所有测试用例的覆盖率都为100%),并且我假设有一个测试用例可以检查GetOvertimeFactor() == 2.0M

不要过度工程师

但是,请不要将此开闭原则作为逻辑上的结论,并且应从一开始就对所有内容进行配置(即通过工程设计)。只定义您当前需要的位。

封闭原则并不妨碍您重新设计该对象。它只是阻止您将当前定义的公共接口更改为对象(受保护的成员是公共接口的一部分)。只要旧功能没有损坏,您仍然可以添加更多功能。


“封闭原则并不妨碍您重新设计该对象。” 实际上,确实如此。如果您读过最初提出开放式封闭原则的书,或者介绍了“ OCP”首字母缩写词的文章,您会看到它说“不允许任何人对其源代码进行更改”(错误除外)修复)。
罗杰里奥

@Rogério:也许是真的(早在1988年)。但是当前的定义(在OO于1990年流行起来时才流行)就是保持一致的公共界面。During the 1990s, the open/closed principle became popularly redefined to refer to the use of abstracted interfaces, where the implementations can be changed and multiple implementations could be created and polymorphically substituted for each other. en.wikipedia.org/wiki/Open/closed_principle
马丁·约克

感谢您的维基百科参考。但是我不确定“当前”定义是否确实有所不同,因为它仍然依赖于类型(类或接口)继承。我提到的“没有源代码更改”的引用来自罗伯特·马丁(Robert Martin)在OCP 1996中发表的文章,该文章(据说)与“当前定义”一致。就个人而言,我认为如果马丁没有给它起首字母缩略词的作用,那么现在就已经忘记了开放式原则,这显然具有很大的营销价值。该原则本身已经过时且有害,IMO。
罗杰里奥

3

因此,Open Closed Principle是一个陷阱,尤其是如果您尝试与YAGNI同时应用时。我如何同时坚持呢?应用三个规则。第一次进行更改时,请直接进行更改。还有第二次。第三次,是时候抽象出变更了。

另一种方法是“一次愚弄我...”,当您必须进行更改时,请应用OCP来防止将来发生更改。我几乎可以建议改变加班费率是一个新故事。“作为工资管理者,我想更改加班费率,以便我可以遵守适用的劳动法。” 现在,您有一个新的UI来更改加班率,一种存储它的方法,GetOvertimeFactor()只是询问其存储库中的加班率是多少。


2

在您发布的示例中,超时因素应为变量或常量。*(Java示例)

class PaycheckCalculator {
   float overtimeFactor;

   protected float setOvertimeFactor(float overtimeFactor) {
      this.overtimeFactor = overtimeFactor;
   }

   protected float getOvertimeFactor() {
      return overtimeFactor;
   }
}

要么

class PaycheckCalculator {
   public static final float OVERTIME_FACTOR = 1.5f;
}

然后,当您扩展类时,设置或覆盖因子。“魔术数字”只能出现一次。这更像是OCP和DRY(不要自己重复)的样式,因为如果使用第一种方法,则不必为不同的因素创建一个全新的类,而只需在一个惯用语言中更改常量即可。排在第二。

在会有多种类型的计算器,每种计算器需要不同的常量值的情况下,我会使用第一种。一个示例就是责任链模式,通常使用继承的类型来实现。只能看到接口的对象(即getOvertimeFactor())使用它来获取所需的所有信息,而子类型则担心要提供的实际信息。

在常量不太可能更改但在多个位置使用常量的情况下,第二个很有用。更改一个常量(在极少数情况下会更改)比在各处进行设置或从属性文件获取要容易得多。

开闭原则不是要不修改现有对象,而是要注意不要更改与它们的接口。如果您需要与类略有不同的行为,或者需要为特定情况添加功能,请扩展和覆盖。但是,如果类本身的要求发生变化(例如更改因子),则需要更改类。庞大的类层次结构没有意义,其中大多数从未使用过。


这是数据更改,而不是代码更改。加班费率不应进行硬编码。
Jim C

您似乎将Get和Set倒退了。
梅森惠勒

哎呀!应该已经测试过……
Michael K

2

我不认为您的示例可以很好地代表OCP。我认为规则的真正含义是:

当您想添加功能时,您只需要添加一个类,而无需修改任何其他类(但也可以修改配置文件)。

下面的实施效果不佳。每次添加游戏时,都需要修改GamePlayer类。

class GamePlayer
{
   public void PlayGame(string game)
   {
      switch(game)
      {
          case "Poker":
              PlayPoker();
              break;

          case "Gin": 
              PlayGin();
              break;

          ...
      }
   }

   ...
}

GamePlayer类永远不需要修改

class GamePlayer
{
    ...

    public void PlayGame(string game)
    {
        Game g = GameFactory.GetByName(game); 
        g.Play();   
    }

    ...
}

现在假设我的GameFactory也遵守OCP,当我想添加另一个游戏时,我只需要构建一个继承自Game该类的新类,一切就可以正常工作。

像第一个这样的类通常都是在经过数年的“扩展”之后才建立起来的,并且从来没有从原始版本中正确地重构过(或更糟糕的是,应该有多个类仍然是一个大类)。

您提供的示例是OCP-ish。我认为,处理加班费率变化的正确方法是在数据库中保留历史费率,以便对数据进行重新处理。该代码仍应关闭以进行修改,因为它将始终从查找中加载适当的值。

作为一个现实世界的示例,我使用了示例的变体,并且“开放式封闭原则”确实令人眼前一亮。功能真的很容易添加,因为我只需要从抽象的基类派生出来,而我的“工厂”就会自动选择它,而“玩家”则不在乎工厂返回的具体实现。


1

在此特定示例中,您具有所谓的“魔术值”。本质上是随时间变化或可能不变化的硬编码值。我将尝试解决您一般性表达的难题,但这只是其中创建子类比更改类中的值所需要做的工作更多。

您很可能在类层次结构中指定行为的时间过早。

假设我们有PaycheckCalculator。这OvertimeFactor很可能会从有关员工的信息中剔除掉。时薪雇员可能会获得加班奖金,而带薪雇员则不会获得任何报酬。尽管如此,一些受薪雇员仍会因为他们正在签订的合同而获得直接工作时间。您可能会决定存在某些已知的薪酬方案类别,这就是建立逻辑的方式。

在基PaycheckCalculator类中,您可以使其抽象,并指定所需的方法。核心计算是相同的,只是某些因素的计算方式不同。HourlyPaycheckCalculator然后,您将实现该getOvertimeFactor方法,并根据情况返回1.5或2.0。您StraightTimePaycheckCalculator将实现getOvertimeFactor返回1.0。最后,第三个实现将是NoOvertimePaycheckCalculator实现getOvertimeFactor返回0的。

关键是仅在打算扩展的基类中描述行为。整个算法的各个部分或特定值的详细信息将由子类填充。您包括getOvertimeFactor潜在客户的默认值的事实会导致快速,轻松地“修复”到一行,而不是按照您的预期扩展类。它还强调了一个事实,即扩展类需要付出很多努力。了解您的应用程序中的类层次结构还需要付出很多努力。您希望以这样一种方式设计类,以最大程度地减少创建子类的需要,同时提供所需的灵活性。

值得深思: 当我们的类像OvertimeFactor您的示例中那样封装某些数据因素时,您可能需要一种从其他来源获取信息的方法。例如,属性文件(因为它看起来像Java)或数据库将保存该值,并且您PaycheckCalculator将使用数据访问对象提取值。这使合适的人员可以更改系统的行为,而无需重写代码。

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.