オープンクローズド原則の利点を活用していますか?


11

Open-Closed Principle(OCP)は、オブジェクトは拡張のために開かれ、修正のために閉じられるべきであると述べています。私はそれを理解し、SRPと組み合わせて使用​​して、たった1つのことを行うクラスを作成すると信じています。そして、すべての動作コントロールをサブクラスで拡張またはオーバーライドできるメソッドに抽出できるようにする多くの小さなメソッドを作成しようとしています。したがって、依存関係の注入と構成、イベント、委任など、多くの拡張ポイントを持つクラスになります。

次の単純で拡張可能なクラスを考えてください。

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

たとえば、OvertimeFactor1.5に変更するとします。上記のクラスは拡張するように設計されているため、簡単にサブクラス化して別のを返すことができますOvertimeFactor

しかし ... ...クラスは拡張用に設計され、OCPに準拠していますが、問題のメソッドをサブクラス化およびオーバーライドしてからIoCコンテナー内のオブジェクトを再配線するのではなく、問題のメソッドを変更します。

その結果、OCPが達成しようとしていることの一部に違反しました。上記の方法は少し簡単なので、怠けているように感じます。OCPを誤解していますか?私は本当に何か違うことをするべきですか?OCPのメリットをさまざまに活用していますか?

更新:回答に基づいて、この人為的な例は、いくつかの異なる理由で貧弱な例のように見えます。この例の主な目的は、オーバーライドされると、内部コードまたはプライベートコードを変更せずにパブリックメソッドの動作を変更するメソッドを提供することにより、クラスが拡張されるように設計されたことを示すことでした。それでも、私は間違いなくOCPを誤解していました。

回答:


9

基本クラスを変更する場合、実際には閉じられていません!

ライブラリを世界にリリースした状況を考えてください。残業率を1.5に変更して基本クラスの動作を変更すると、クラスが閉じられていると想定してコードを使用するすべてのユーザーに違反していることになります。

本当にクラスを閉じているが開いているようにするには、別のソース(設定ファイルかもしれません)から残業要因を取得するか、オーバーライドできる仮想メソッドを証明する必要がありますか?

クラスが本当に閉じられていた場合、変更後にテストケースは失敗せず(すべてのテストケースで100%のカバレッジがあると仮定)、チェックするテストケースがあると仮定しますGetOvertimeFactor() == 2.0M

エンジニアをしすぎないでください

ただし、このオープンクローズの原則を論理的な結論に至らしめ、最初からすべてを構成可能にしてはなりません(つまり、エンジニアリングを超えています)。現在必要なビットのみを定義します。

閉じた原則は、オブジェクトの再設計を妨げるものではありません。オブジェクトに対して現在定義されているパブリックインターフェイスを変更できないようにするだけです(保護されたメンバーはパブリックインターフェイスの一部です)。古い機能が壊れていない限り、さらに機能を追加できます。


「閉じた原則は、オブジェクトのリエンジニアリングを妨げるものではありません。」実際、そうです。Open-Closed Principleが最初に提案された本、または「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

したがって、オープンクローズドプリンシパルは、特にYAGNIと同時に適用しようとする場合は注意が必要です。両方を同時に遵守するにはどうすればよいですか?3ルールを適用します。初めて変更を行うときは、直接変更してください。そして二度目も。3回目は、その変化を抽象化するときです。

別のアプローチは「一度だます...」です。変更が必要な場合は、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(Do n't Repeat Yourself)のスタイルです。最初のメソッドを使用し、1つの慣用句で定数を変更するだけで、別の要因に対してまったく新しいクラスを作成する必要がないためです。 2番目に配置します。

複数のタイプの計算機があり、それぞれが異なる定数値を必要とする場合に最初のものを使用します。例は、通常、継承された型を使用して実装される責任の連鎖パターンです。getOvertimeFactor()サブタイプは提供する実際の情報を心配しながら、インターフェースのみを見ることができる(つまり)オブジェクトは、それを使用して必要なすべての情報を取得します。

2番目は、定数が変更される可能性が低いが、複数の場所で使用される場合に役立ちます。1つの定数を変更すること(まれなケースですが)は、場所全体に設定するか、プロパティファイルから取得するよりもはるかに簡単です。

Open-closedの原則は、既存のオブジェクトを変更しないという呼び出しではなく、既存のオブジェクトへのインターフェイスを変更しないという注意よりも重要です。クラスと若干異なる動作が必要な場合、または特定のケースに機能を追加する必要がある場合は、拡張およびオーバーライドします。ただし、クラス自体の要件が変更された場合(係数の変更など)、クラスを変更する必要があります。巨大なクラス階層には意味がなく、そのほとんどは決して使用されません。


これはデータの変更であり、コードの変更ではありません。残業率はハードコードされていてはなりません。
ジムC

GetとSetが逆になっているようです。
メイソンウィーラー

おっと!...テストしている必要があります
マイケル・K

2

OCPの優れた表現としてあなたの例を本当に見ていません。私はルールが本当に意味するものはこれだと思う:

機能を追加する場合、1つのクラスを追加するだけでよく、他のクラス(ただし、構成ファイル)を変更する必要はありません。

下の貧しい実装。ゲームを追加するたびに、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だけで、すべてが正常に機能するはずです。

多くの場合、最初のようなクラスは何年もの「拡張」の後に構築され、元のバージョンから正しくリファクタリングされません(または、さらに悪いことに、複数のクラスが1つの大きなクラスのままになります)。

提供する例はOCPに似ています。私の意見では、残業率の変化を処理する正しい方法は、データを再処理できるように履歴率を保持したデータベース内にあることです。ルックアップから常に適切な値をロードするため、コードは変更のために閉じたままにする必要があります。

実世界の例として、私は私の例の変種を使用しました、そして、Open-Closed Principleは本当に輝いています。機能を追加するのは本当に簡単です。抽象基本クラスから派生するだけで、「ファクトリ」がそれを自動的に取得し、「プレーヤー」はファクトリが返す具体的な実装を気にしないからです。


1

この特定の例では、「マジック値」と呼ばれるものがあります。基本的に、時間とともに変化する場合と変化しない場合があるハードコードされた値。私はあなたが一般的に表現する難問に対処しようとしますが、これはサブクラスの作成がクラスの値を変更するよりも多くの仕事のタイプの例です。

おそらく、クラス階層の早い段階で動作を指定しました。

があるとしましょうPaycheckCalculatorOvertimeFactor以上の可能性が高い従業員に関する情報をキーオフされるだろう。時間給制の従業員は残業ボーナスを享受できますが、給料を支払った従業員には何も支払われません。それでも、給与を支払っている従業員の中には、契約していたために、まっすぐな時間を得る人もいます。特定の既知の支払シナリオのクラスがあると判断する場合があり、それがロジックの構築方法です。

基本PaycheckCalculatorクラスで抽象化し、必要なメソッドを指定します。コアの計算は同じで、特定の要因が異なる方法で計算されるだけです。あなたはHourlyPaycheckCalculatorその後、実装してgetOvertimeFactorメソッドをし、あなたの場合に応じて1.5または2.0を返します。あなたはStraightTimePaycheckCalculator実装してgetOvertimeFactor1.0を返すこと。最後に、3番目の実装は、0を返すNoOvertimePaycheckCalculatorを実装するgetOvertimeFactorものです。

重要なのは、拡張が意図されている基本クラスの動作のみを記述することです。アルゴリズム全体の一部または特定の値の詳細は、サブクラスによって入力されます。getOvertimeFactorリードにデフォルト値を含めたという事実は、意図したとおりにクラスを拡張する代わりに、1行をすばやく簡単に「修正」することにつながります。また、クラスの拡張に関連する努力があるという事実も強調しています。また、アプリケーションのクラスの階層を理解するための努力も必要です。サブクラスを作成する必要性を最小限に抑えながら、必要な柔軟性を提供するような方法でクラスを設計します。

OvertimeFactor参考までに例の ようにクラスが特定のデータ要素をカプセル化する場合、他のソースからその情報を引き出す方法が必要になる場合があります。たとえば、プロパティファイル(Javaのように見えるため)またはデータベースが値を保持しPaycheckCalculator、データアクセスオブジェクトを使用して値を取得します。これにより、適切な人がコードの書き換えを必要とせずにシステムの動作を変更できます。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.