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


8

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

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

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

だから質問は

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

1
ここでの問題は、あなたのチケットクラスがそれ自体を印刷する方法を知らないはずだということです。TicketPrinterクラスはチケットを受け取り、印刷方法を知っている必要があります。この場合、decotatorパターンを使用してプリンタークラスをインクリメントするか、それをオーバーライドして完全に変更できます。

1
デコレータも同じオープン/クローズの問題に直面します。要件は将来何度も変更される可能性があります。その場合、デコレータを変更するか、デコレータを拡張します。毎回デコレータを拡張することは現実的ではないようで、デコレータを変更すると開閉の原則に違反します。
2016

1
OCP で設計を任意に変更して設計することは非常に困難です。安定しているパーツに対して変化すると予想される寸法を選択するのは賭けです。
Fuhrmanator

回答:


5

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

  • 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)";
    }
}

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

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

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

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

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


はい、この特定のケースではテンプレートが機能します。しかし、これは問題を解決するための単なる例です。テンプレート化/プラグインメソッドが適用できない多くのシナリオ/ビジネス上の問題が存在する可能性があります。
2016

1
クラスインターフェイスに関するあなたのポイントは、私の意見では、この特定のシナリオに対する最良の解決策です。特定の質問に焦点を合わせるには、チケットクラスのインターフェイスで定義された出力を(定義された入力と共に)定義したPrintTicketメソッドを用意し、チケットの出力が常に同じである限り、どのようにしても問題ありません。基礎となるクラスが変更されました。-また、チケットのプロパティが常に変更されることが予想される場合は、それを既知のビジネス要件としてクラスを設計してみませんか。チケットにはある種のticketPropertyオブジェクトのコレクションを含める必要があります
user3908435

TicketPrinterコンストラクタに渡されるクラスを作成することもできます。
Zymus

2

オープン-クローズド原理の単純なラインから始めましょう- "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

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

これは文字通りSRP問題の教科書の例であるか、ウィキペディアに次のように記載されています。

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

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

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


1

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

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


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

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

これで、このクラスを継承し、検証メソッドをオーバーライドして常に戻るだけで、このクラスを「解除」できますtrue。ポリモーフィズムが原因で、使用A可能な場所であればどこでもサブクラスを使用でき、プログラムで望ましくない動作が発生する可能性があります。

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

たとえば、検証メソッドを作成することでこれを行うことができますfinal


これは違反であると述べるOCPの説明を見たことがありません。LSP、間違いなく、OCPではありません。
ジュール

@Jules私は後でウィキペディアの定義が確かにこれとは非常に異なっていることを発見しました。これは、OCPを教えるために使用された例の1つでした(ただし、多少異なる場合がありました)。ポイントは、コードの変更を禁止することではありませんでした。それも私には意味がありません。コードを記述し、環境が変化してコードが適合しなくなった場合は、コードを変更するか、それ以上に変更して、コードを破棄してやり直してください。なぜ壊れたものを保管しておくのか...
Jorn Vernee

0

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

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

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

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