web-dev-qa-db-ja.com

開閉原理のメリットを活用していますか?

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

次の単純で拡張可能なクラスについて考えてみます。

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

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

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

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

更新:この不自然な例は、さまざまな理由から不適切な例であるように思われる回答に基づいています。この例の主な目的は、オーバーライドされたときにpublicメソッドの動作を変更するメソッドを提供することにより、クラスが拡張されるように設計されていることを示すことでした。 internalまたはプライベートコードを変更する必要があります。それでも、私は間違いなくOCPを誤解しています。

12
Kaleb Pederson

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

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

本当にクラスをクローズしてオープンにするためには、別のソース(おそらく設定ファイル)から残業係数を取得するか、オーバーライドできる仮想メソッドを証明する必要がありますか?

クラスが本当にクローズされた場合、変更後のテストケースは失敗せず(すべてのテストケースが100%カバーされていると想定)、GetOvertimeFactor() == 2.0Mをチェックするテストケースがあると思います。

エンジニアを超えないでください

ただし、この開閉の原則を論理的な結論に至らせないでください。また、最初からすべてを構成可能にしてください(つまり、エンジニアリング以上)。現在必要なビットのみを定義します。

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

10
Martin York

したがって、Open Closed Principleは、特に [〜#〜] yagni [〜#〜] と同時に適用しようとする場合、問題になります。同時に両方を遵守するにはどうすればよいですか? つのルール を適用します。初めて変更する場合は、直接変更してください。そして二度目も。 3回目は、その変化を抽象化する時です。

もう1つのアプローチは、「一度私をだます...」です。変更を加える必要がある場合は、OCPを適用して将来の変更から保護します。残業代の変更は新しい話だと提案するところまで、ほとんど行きません。 「給与管理者として、私は残業代を変更して、適用される労働法に準拠できるようにしたいと考えています。」これで、残業代を変更するための新しいUIがあり、それを保存する方法が得られました。GetOvertimeFactor()は、そのリポジトリに残業代が何であるかを尋ねるだけです。

3
Michael Brown

私はあなたの例を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風です。私の考えでは、残業代の変更を処理する正しい方法は、データを再処理できるように履歴レートが保持されているデータベース内にあるでしょう。コードは常にルックアップから適切な値をロードするため、変更のために閉じておく必要があります。

実世界の例として、私は自分の例の変形を使用しており、開閉原理が本当に輝いています。抽象基本クラスから派生する必要があり、「ファクトリー」がそれを自動的に取得し、「プレーヤー」がファクトリーが返す具体的な実装を気にしないため、機能は本当に簡単に追加できます。

2
Austin Salonen

投稿した例では、時間外要因は変数または定数でなければなりません。 *(Javaの例)

_class PaycheckCalculator {
   float overtimeFactor;

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

   protected float getOvertimeFactor() {
      return overtimeFactor;
   }
}
_

OR

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

次に、クラスを拡張するときに、係数を設定またはオーバーライドします。 「魔法の数字」は一度だけ現れるべきです。これは、OCPとDRY(Do n't Repeat Yourself)のスタイルのほうがはるかに優れています。最初のメソッドを使用する場合、別の要因に対してまったく新しいクラスを作成する必要がなく、1つの慣用法で定数を変更するだけでよいからです。 2番目に配置します。

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

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

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

2
Michael K

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

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

PaycheckCalculatorがあるとします。 OvertimeFactorは、おそらく従業員に関する情報から除外されます。時間給の従業員は残業ボーナスを享受できますが、給与のある従業員は何も支払われません。それでも、一部の給与所得者は、彼らが取り組んでいた契約のために、まっすぐな時間を取得します。特定の既知の給与シナリオのクラスがあり、それがロジックの構築方法です。

ベースのPaycheckCalculatorクラスでは、それを抽象化し、期待するメソッドを指定します。コアの計算は同じですが、特定の要因が異なる方法で計算されるだけです。 HourlyPaycheckCalculatorgetOvertimeFactorメソッドを実装し、場合によっては1.5または2.0を返します。 StraightTimePaycheckCalculatorgetOvertimeFactorを実装して1.0を返します。最後に、3番目の実装はNoOvertimePaycheckCalculatorであり、getOvertimeFactorを実装して0を返します。

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

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

1
Berin Loritsch