web-dev-qa-db-ja.com

設計パターンのオープンクローズの原則

オープンクローズの原則を実際に適用する方法について少し混乱しています。時間の経過とともにビジネスの要件は変化します。 Open-Closedの原則に従って、既存のクラスを変更する代わりにクラスを拡張する必要があります。クラスを延長するたびに、要件を満たすのは現実的ではないように思えます。列車予約システムの例を挙げましょう。

列車予約システムには、チケットオブジェクトがあります。通常のチケット、割引チケットなど、さまざまなタイプのチケットが存在する可能性があります。チケットは抽象クラスで、RegularTicketとConcessionTicketsは具象クラスです。すべてのチケットには共通のPrintTicketメソッドがあるため、基本抽象クラスであるTicketで記述されます。これが数か月間うまくいったとしましょう。次に、チケットのフォーマットを変更することを要求する新しい要件が発生します。印刷されたチケットにいくつかのフィールドが追加されるか、形式が変更される場合があります。この要件を満たすために、次のオプションがあります

  1. チケット抽象クラスのPrintTicket()メソッドを変更します。しかし、これは開閉原理に違反します。
  2. 子クラスのPrintTicket()メソッドをオーバーライドしますが、これにより、DRY(自分自身を繰り返さない)原則に違反する印刷ロジックが複製されます。

質問があるので

  1. オープン/クローズドの原則に違反せずに、上記のビジネス要件を満たすにはどうすればよいですか。
  2. クラスが変更のために閉じられることになっているとき?クラスが変更のために閉鎖されていると見なすための基準は何ですか?それは、クラスの初期実装後ですか、それとも本番環境での最初のデプロイメント後か、それとも別のものかもしれません。
8
parag

次のオプションがあります

  • チケット抽象クラスのPrintTicket()メソッドを変更します。しかし、これは開閉原理に違反します。
  • 子クラスのPrintTicket()メソッドをオーバーライドしますが、これは印刷ロジックを複製するため、DRY(自分自身を繰り返さない)の原則に違反します。

これは、PrintTicketの実装方法によって異なります。メソッドがサブクラスからの情報を考慮する必要がある場合、メソッドが追加情報を提供する方法を提供する必要があります。

さらに、コードを繰り返すことなくオーバーライドすることもできます。たとえば、実装から基本クラスメソッドを呼び出す場合は、繰り返しを避けます。

class ConcessionTicket : Ticket {
    public override string PrintTicket() {
        return $"{base.PrintTicket()} (concession)"
    }
}

オープン/クローズドの原則に違反せずに上記のビジネス要件を満たすにはどうすればよいですか?

テンプレートメソッド パターンは3番目のオプションを提供します。基本クラスにPrintTicketを実装し、派生クラスに依存します必要に応じて追加の詳細を提供します。

クラス階層を使用した例を次に示します。

abstract class Ticket {
    public string Name {get;}
    public string Train {get;}
    protected virtual string AdditionalDetails() {
        return "";
    }
    public string PrintTicket() {
        return $"{Name} : {Train}{AdditionalDetails()}";
    }
}

class RegularTicket : Ticket {
    ... // Uses the default implementation of AdditionalDetails()
}


class ConcessionTicket : Ticket {
    protected override string AdditionalDetails() {
        return " (concession)";
    }
}

クラスが変更のために閉鎖されていると見なすための基準は何ですか?

変更に閉じる必要があるのはクラスではなく、そのクラスのインターフェースです(つまり、広い意味での「インターフェース」、つまり、メソッドやプロパティのコレクション、およびそれらの動作であり、言語構造ではありません)。クラスはその実装を非表示にするため、クラスの所有者は、外部から見える動作が変更されない限り、いつでもクラスを変更できます。

それはクラスの最初の実装の後ですか、それとも本番環境での最初のデプロイメントの後か、それとも何か他のものかもしれませんか?

クラスのインターフェースは、外部で使用するために初めて公開した後、閉じたままにする必要があります。内部使用クラスは、その使用法をすべて見つけて修正できるので、いつまでもリファクタリングを受け入れることができます。

最も単純なケースを除いて、特定の数のリファクタリングの反復後、クラス階層がすべての可能な使用シナリオをカバーすることを期待することは現実的ではありません。基本クラスで完全に新しいメソッドを呼び出す追加の要件が定期的に発生するため、クラスはいつでも変更できるようになります。

5
dasblinkenlight

問題は、オープン/クローズドの原則に違反しているのではなく、単一責任の原則に違反していることです。

これは文字通りSRP問題の教科書の例、または wikipedia の状態として:

例として、レポートをコンパイルして出力するモジュールを考えます。このようなモジュールは2つの理由で変更できると想像してください。まず、レポートの内容が変更される可能性があります。次に、レポートの形式が変更される可能性があります。これら2つのことは、非常に異なる原因によって変化します。 1つは実体的、もう1つは化粧品です。単一の責任原則は、問題のこれらの2つの側面は実際には2つの別個の責任であるため、別個のクラスまたはモジュールにあるべきであると述べています。異なる時期に異なる理由で変化する2つのものを組み合わせるのは、悪い設計です。

新しい情報を追加するため、さまざまなチケットクラスが変更される場合があります。新しいレイアウトが必要なため、チケットの印刷が変更される可能性があります。したがって、入力としてチケットを受け入れるTicketPrinterクラスが必要です。

チケットクラスは、さまざまなタイプのデータを提供したり、ある種のデータテンプレートを提供したりするために、さまざまなインターフェースを実装できます。

2
Bjorn

オープン-クローズド原理の簡単な行から始めましょう-"Open for extension Closed for modification"。あなたの問題を考えてみましょう。クラスを設計して、ベースチケットが共通のチケット情報を印刷し、他のチケットタイプがベース印刷の上に独自のフォーマットを印刷するようにします。

abstract class Ticket
{
    public string Print()
    {
        // Print base properties here. and return 
        return PrintBasicInfo() + AddionalPrint();
    }

    protected abstract string AddionalPrint();

    private string PrintBasicInfo()
    {
        // print base ticket info
    }
}

class ConcessionTicket : Ticket
{
    protected override string AddionalPrint()
    {
        // Specific to Conecession ticket printing
    }
}

class RegularTicket : Ticket
{
    protected override string AddionalPrint()
    {
        // Specific to Regular ticket printing
    }
}

このように、システムに導入される新しいタイプのチケットを適用すると、基本的なチケット印刷情報に加えて、独自の印刷機能が実装されます。ベースチケットは現在、変更のためにクローズされていますが、派生タイプに追加のメソッドを提供することによって拡張のためにオープンされています。

2
vendettamit

チケット抽象クラスのPrintTicket()メソッドを変更します。しかし、これは開閉原理に違反します。

これは、オープンクローズプリンシパルに違反しません。コードは常に変更されるため、SOLIDが重要です。そのため、コードは保守可能、柔軟、変更可能です。コードの変更に問題はありません。


OCPは、クラスの目的の機能を改ざんできない外部クラスに関するものです。

たとえば、コンストラクタにStringを受け取るクラスAがあり、メソッドを呼び出して文字列を確認することにより、このStringが特定の形式であることを確認します。この検証方法はAのクライアントでも使用できる必要があるため、単純な実装ではpublicになっています。

これで、このクラスを継承し、検証メソッドをオーバーライドして常にtrueを返すようにすることで、このクラスを「解除」できます。多態性のため、Aを使用できる場所ならどこでもサブクラスを使用できます。プログラムで望ましくない動作が発生する可能性があります。

これは悪意のある行為を静かにするように見えますが、より複雑なコードベースでは、「正直な間違い」として行われる可能性があります。 OCPに準拠するために、クラスAは、この誤りを犯さないように設計する必要があります。

これは、たとえば、検証メソッドをfinalにすることで実行できます。

1
Jorn Vernee

継承はOCPの要件を満たすために使用できる唯一のメカニズムではありません。ほとんどの場合、それが最善であるとはめったにないと思います-少し前もって計画を検討する限り、通常はより良い方法があります。必要になる可能性が高い変更の種類。

この場合、チケット印刷システムのフォーマットを頻繁に変更することが予想される場合(そして、私にとってはかなり安全な方法のように思えます)、テンプレートシステムを使用することには大きな意味があると私は主張します。コードを変更する必要はまったくありません。この要求を満たすために必要なのは、テンプレートを変更することだけです(とにかく、テンプレートシステムが必要なすべてのデータにアクセスできる限り)。

実際には、システムを変更に対して完全に閉じることはできません。システムを閉じることは、多大な無駄な労力を必要とするため、どのような変更が発生する可能性があるかを判断し、それに応じてコードを構造化する必要があります。

0
Jules