web-dev-qa-db-ja.com

LSP vs OCP / Liskov Substitution VS Open Close

SOLID OOPの原則)を理解しようとしています。LSPとOCPにはいくつかの類似点があると結論しました(詳細は言わない限り) 。

オープン/クローズの原則は、「ソフトウェアエンティティ(クラス、モジュール、関数など)は拡張のためにオープンし、変更のためにクローズする必要がある」と述べています。

単純な言葉でのLSPは、FooのインスタンスはBarから派生したFooのインスタンスで置き換えることができ、プログラムはまったく同じように動作することを示しています。

私はプロではありませんOOPプログラマーですが、LSPは、Barから派生したFooがその中の何も変更しない場合にのみ可能であるように思えますつまり、特定のプログラムでは、LSPは、OCPがtrueの場合にのみtrueであり、OCPは、LSPがtrueの場合にのみtrueであることを意味します。

私が間違っていたら訂正してください。私はこれらの考えを本当に理解したいです。答えてくれてありがとう。

49
Kolyunya

おっと、OCPとLSPについては奇妙な誤解がいくつかあり、一部はいくつかの用語の不一致と紛らわしい例が原因です。同じ方法で実装した場合、両方の原則は「同じもの」にすぎません。パターンは通常、いくつかの例外を除いて、何らかの方法で原則に従います。

違いについては後で詳しく説明しますが、まず原則自体について詳しく見ていきましょう。

開閉原理(OCP)

ボブおじさん によると:

クラスの動作を変更せずに拡張できるはずです。

この場合のWordextendは、必ずしも新しい動作を必要とする実際のクラスをサブクラス化する必要があることを意味しないことに注意してください。用語の最初の不一致で私がどのように述べたかを見てくださいキーワードextendは、Javaでのサブクラス化のみを意味しますが、原則はJavaより古いものです。

オリジナルは1988年にバートランド・マイヤーから来た:

ソフトウェアエンティティ(クラス、モジュール、関数など)は、拡張のために開いている必要がありますが、変更のために閉じている必要があります。

ここで、原理がソフトウェアエンティティに適用されていることがより明確になります。悪い例は、拡張ポイントを提供するのではなく、コードを完全に変更しているときにソフトウェアエンティティをオーバーライドすることです。ソフトウェアエンティティ自体の動作は拡張可能である必要があり、この良い例は Strategy-pattern の実装です(これは、GoFパターンの束IMHOを示すのが最も簡単であるため)。

_// Context is closed for modifications. Meaning you are
// not supposed to change the code here.
public class Context {

    // Context is however open for extension through
    // this private field
    private IBehavior behavior;

    // The context calls the behavior in this public 
    // method. If you want to change this you need
    // to implement it in the IBehavior object
    public void doStuff() {
        if (this.behavior != null)
            this.behavior.doStuff();
    }

    // You can dynamically set a new behavior at will
    public void setBehavior(IBehavior behavior) {
        this.behavior = behavior;
    }
}

// The extension point looks like this and can be
// subclassed/implemented
public interface IBehavior {
    public void doStuff();
}
_

上記の例では、Contextlockedに変更されています。ほとんどのプログラマーは、クラスを拡張するためにクラスをサブクラス化することを望みますが、ここでは、IBehaviorインターフェースを実装するものを通じて動作がchangedできると想定しているため、ここでは行いません。

つまりコンテキストクラスは変更のために閉じられていますが、拡張のために開かれています。継承ではなくオブジェクト構成でビヘイビアを配置するため、実際には別の基本原則に従います。

「 ' クラス継承 'よりも ' オブジェクト構成 'を優先する。」 (ギャングオブフォー1995:20)

それはこの質問の範囲外なので、読者にその原則について読んでもらいましょう。例を続けるために、IBehaviorインターフェースの次の実装があるとします。

_public class HelloWorldBehavior implements IBehavior {
    public void doStuff() {
        System.println("Hello world!");
    }
}

public class GoodByeBehavior implements IBehavior {
    public void doStuff() {
        System.out.println("Good bye cruel world!");
    }
}
_

このパターンを使用すると、拡張ポイントとしてsetBehaviorメソッドを使用して、実行時のコンテキストの動作を変更できます。

_// in your main method
Context c = new Context();

c.setBehavior(new HelloWorldBehavior());
c.doStuff();
// prints out "Hello world!"

c.setBehavior(new GoodByeBehavior());
c.doStuff();
// prints out "Good bye cruel world!"
_

したがって、「クローズされた」コンテキストクラスを拡張する場合は常に、「オープンな」協調依存関係をサブクラス化することでそれを行います。これは明らかにコンテキスト自体をサブクラス化することと同じではありませんが、OCPです。 LSPもこれについて言及していません。

継承ではなくミックスインで拡張する

サブクラス化以外のOCPを行う方法は他にもあります。 1つの方法は、mixinsを使用して、拡張のためにクラスを開いたままにすることです。これは便利です。クラスベースではなくプロトタイプベースの言語で。アイデアは、動的オブジェクトを必要に応じてより多くのメソッドまたは属性で修正することです。つまり、他のオブジェクトとブレンドまたは「ミックスイン」するオブジェクトです。

アンカー用のシンプルなHTMLテンプレートをレンダリングするミックスインのJavaScriptの例を次に示します。

_// The mixin, provides a template for anchor HTML elements, i.e. <a>
var LinkMixin = {
    render: function() {
        return '<a href="' + this.link +'">'
            + this.content 
            + '</a>;
    }
}

// Constructor for a youtube link
var YoutubeLink = function(content, youtubeId) {
    this.content = content;
    this.setLink(this.youtubeId);
};
// Methods are added to the prototype
YoutubeLink.prototype = {
    setLink: function(youtubeid) {
        this.link = 'http://www.youtube.com/watch?v=' + youtubeid;
    }
};
// Extend YoutubeLink prototype with the LinkMixin using
// underscore/lodash extend
_.extend(YoutubeLink.protoype, LinkMixin);

// When used:
var ytLink = new YoutubeLink("Cool Movie!", "idOaZpX8lnA");

console.log(ytLink.render());
// will output: 
// <a href="http://www.youtube.com/watch?=vidOaZpX8lnA">Cool Movie!</a>
_

アイデアは、オブジェクトを動的に拡張することであり、これの利点は、オブジェクトが完全に異なるドメインにある場合でも、オブジェクトがメソッドを共有できることです。上記の場合、LinkMixinを使用して特定の実装を拡張することにより、他の種類のHTMLアンカーを簡単に作成できます。

OCPに関しては、「ミックスイン」は拡張機能です。上記の例では、YoutubeLinkは変更のために閉じられているが、ミックスインを使用して拡張のために開かれているソフトウェアエンティティです。オブジェクト階層がフラット化されているため、タイプをチェックできません。しかし、これは実際に悪いことではありません。後で詳しく説明しますが、型のチェックは一般に悪い考えであり、多態性によってその考えを壊します。

ほとんどのextend実装は複数のオブジェクトを混在させることができるため、このメソッドで複数の継承を行うことが可能であることに注意してください。

__.extend(MyClass, Mixin1, Mixin2 /* [, ...] */);
_

覚えておく必要があるのは、名前を衝突させないことだけです。つまり、ミックスインは、オーバーライドされるため、いくつかの属性またはメソッドに同じ名前を定義することがあります。私のささやかな経験では、これは問題ではなく、それが発生した場合は、設計に欠陥があることを示しています。

リスコフの代替原則(LSP)

ボブおじさんはそれを次のように簡単に定義しています:

派生クラスは、基本クラスの代わりに使用できる必要があります。

この原則は古く、実際にはボブおじさんの定義は原則を区別していません。それは、上記の戦略の例では同じスーパータイプが使用されているという事実(IBehavior)によって、LSPがOCPと密接に関連しているためです。それでは、Barbara Liskovによる元の定義を見て、数学の定理のように見えるこの原理について他に何かを見つけることができるかどうかを見てみましょう。

ここで必要なのは、次の置換プロパティのようなものです。S型のオブジェクト_o1_がT型のオブジェクト_o2_である場合、すべてのプログラムでPTで定義されている場合、動作_o1_が_o2_に置き換えられた場合、Pは変更されず、STのサブタイプです。

これについてはしばらく肩をすくめてみましょう。クラスについてはまったく触れていないので注意してください。 JavaScriptでは、明示的にクラスベースではない場合でも、LSPを実際に追跡できます。プログラムに、次のような少なくともいくつかのJavaScriptオブジェクトのリストがある場合:

  • 同じ方法で計算する必要があります、
  • 同じ動作をし、
  • そうでなければ、ある意味で完全に異なる

...その後、オブジェクトは同じ「タイプ」を持っていると見なされ、プログラムにとって実際には問題になりません。 これは本質的に polymorphism一般的な意味でです。インターフェイスを使用している場合は、実際のサブタイプを知っている必要はありません。 OCPはこれについて明確に述べていません。また、実際には、ほとんどの初心者プログラマーが行う設計ミスを正確に指摘しています。

オブジェクトのサブタイプをチェックしたいときはいつでも、間違っている可能性が高いです。

わかりましたので、それは常に間違っているわけではありませんが、instanceofまたは列挙型を使用していくつかの型チェックを実行したい場合は、プログラムをもう少し複雑にしている可能性があります必要以上にあなた自身。しかし、これは常に当てはまるわけではありません。解決策が十分に小さい場合、物事を機能させるための迅速で汚いハックは私の心に許して大丈夫です。そして、あなたが 無慈悲なリファクタリング を実践している場合、変更がそれを要求すると、それは改善されるかもしれません。

実際の問題に応じて、この「設計ミス」を回避する方法があります。

  • スーパークラスは前提条件を呼び出さず、代わりに呼び出し元に強制します。
  • スーパークラスには、呼び出し元が必要とするジェネリックメソッドがありません。

これらは両方とも、一般的なコード設計の「間違い」です。 プルアップメソッド などのいくつかの異なるリファクタリング、または Visitor pattern などのパターンへのリファクタリングがあります。

大きなif文のスパゲッティを処理でき、既存のコードで考えているよりも実装が簡単なため、ビジターパターンは実際にとても気に入っています。次のコンテキストがあるとします。

_public class Context {

    public void doStuff(string query) {

        // outcome no. 1
        if (query.Equals("Hello")) {
            System.out.println("Hello world!");
        } 

        // outcome no. 2
        else if (query.Equals("Bye")) {
            System.out.println("Good bye cruel world!");
        }

        // a change request may require another outcome...

    }

}

// usage:
Context c = new Context();

c.doStuff("Hello");
// prints "Hello world"

c.doStuff("Bye");
// prints "Bye"
_

Ifステートメントの結果は、それぞれの決定と実行するコードに依存するため、それぞれの訪問者に変換できます。これらを次のように抽出できます。

_public interface IVisitor {
    public bool canDo(string query);
    public void doStuff();
}

// outcome 1
public class HelloVisitor implements IVisitor {
    public bool canDo(string query) {
        return query.Equals("Hello");
    }
    public void doStuff() {
         System.out.println("Hello World");
    }
}

// outcome 2
public class ByeVisitor implements IVisitor {
    public bool canDo(string query) {
        return query.Equals("Bye");
    }
    public void doStuff() {
        System.out.println("Good bye cruel world");
    }
}
_

この時点で、プログラマーが訪問者パターンを知らなかった場合は、代わりにContextクラスを実装して、特定のタイプかどうかを確認します。 Visitorクラスにはブール型のcanDoメソッドがあるため、実装者はそのメソッド呼び出しを使用して、それがジョブを実行するのに適切なオブジェクトかどうかを判断できます。コンテキストクラスは、次のようにすべての訪問者を使用できます(新しい訪問者を追加できます)。

_public class Context {
    private ArrayList<IVisitor> visitors = new ArrayList<IVisitor>();

    public Context() {
        visitors.add(new HelloVisitor());
        visitors.add(new ByeVisitor());
    }

    // instead of if-statements, go through all visitors
    // and use the canDo method to determine if the 
    // visitor object is the right one to "visit"
    public void doStuff(string query) {
        for(IVisitor visitor : visitors) {
            if (visitor.canDo(query)) {
                visitor.doStuff();
                break;
                // or return... it depends if you have logic 
                // after this foreach loop
            }
        }
    }

    // dynamically adds new visitors
    public void addVisitor(IVisitor visitor) {
        if (visitor != null)
            visitors.add(visitor);
    }
}
_

どちらのパターンもOCPとLSPに従いますが、どちらもパターンの異なる点を正確に示しています。では、原則に違反した場合、コードはどのように見えるのでしょうか。

1つの原則に違反するが、他の原則に従う

原則の1つを破る方法がありますが、それでも他の原則に従う必要があります。以下の例は、正当な理由により、不自然なように見えますが、実際にこれらが本番コードでポップアップする(さらに悪い)のを目にしました。

OCPをフォローしますが、LSPはフォローしません

与えられたコードがあるとしましょう:

_public interface IPerson {}

public class Boss implements IPerson {
    public void doBossStuff() { ... }
}

public class Peon implements IPerson {
    public void doPeonStuff() { ... }
}

public class Context {
    public Collection<IPerson> getPersons() { ... }
}
_

このコードは、開閉の原則に従っています。コンテキストのGetPersonsメソッドを呼び出すと、独自の実装を持つすべての人が取得されます。つまり、IPersonは変更のために閉じていますが、拡張のために開いています。しかし、私たちがそれを使わなければならないとき、物事は暗転します:

_// in some routine that needs to do stuff with 
// a collection of IPerson:
Collection<IPerson> persons = context.getPersons();
for (IPerson person : persons) {
    // now we have to check the type... :-P
    if (person instanceof Boss) {
        ((Boss) person).doBossStuff();
    }
    else if (person instanceof Peon) {
        ((Peon) person).doPeonStuff();
    }
}
_

型チェックと型変換を行う必要があります!タイプチェックがどのように悪いことであるかを前述した方法を覚えていますか大野!しかし、恐れることはありません。前述のように、プルアップリファクタリングを行うか、ビジターパターンを実装します。この場合、一般的なメソッドを追加した後、単純にプルアップリファクタリングを実行できます。

_public class Boss implements IPerson {
    // we're adding this general method
    public void doStuff() {
        // that does the call instead
        this.doBossStuff();
    }
    public void doBossStuff() { ... }
}


public interface IPerson {
    // pulled up method from Boss
    public void doStuff();
}

// do the same for Peon
_

ここでの利点は、LSPに従って正確な型を知る必要がなくなったことです。

_// in some routine that needs to do stuff with 
// a collection of IPerson:
Collection<IPerson> persons = context.getPersons();
for (IPerson person : persons) {
    // yay, no type checking!
    person.doStuff();
}
_

LSPをフォローしますが、OCPはフォローしません

OCPではなくLSPに続くいくつかのコードを見てみましょう。それは一種の不自然なものですが、このコードについては非常に微妙な間違いです。

_public class LiskovBase {
    public void doStuff() {
        System.out.println("My name is Liskov");
    }
}

public class LiskovSub extends LiskovBase {
    public void doStuff() {
        System.out.println("I'm a sub Liskov!");
    }
}

public class Context {
    private LiskovBase base;

    // the good stuff
    public void doLiskovyStuff() {
        base.doStuff();
    }

    public void setBase(LiskovBase base) { this.base = base }
}
_

コンテキストは実際の型を知らなくてもLiskovBaseを使用できるため、コードはLSPを実行します。このコードもOCPに従っていると思いますが、よく見ると、クラスは本当にclosedですか? doStuffメソッドが行を出力するだけではなかった場合はどうなりますか?

OCPに従う場合の答えは単純です:[〜#〜] no [〜#〜]、これはそうではありません。このオブジェクト設計では、コードを完全に別のものでオーバーライドする必要があります。これにより、基本クラスからコードをコピーして機能させるために、ワームのカットアンドペーストができるようになります。 doStuffメソッドは拡張用に開いていますが、変更のために完全に閉じられていません。

これに テンプレートメソッドパターン を適用できます。テンプレートメソッドパターンはフレームワークで非常に一般的であるため、知らないうちに使用していた可能性があります(例Java Swingコンポーネント、c#フォームおよびコンポーネントなど)。これは、閉じる1つの方法ですdoStuffメソッドを変更し、Javaのfinalキーワードでマークすることによって閉じたままにすることを確認します。このキーワードは、クラスをさらにサブクラス化することを防ぎます(C#では sealed を使用して同じことができます)。

_public class LiskovBase {
    // this is now a template method
    // the code that was duplicated
    public final void doStuff() {
        System.out.println(getStuffString());
    }

    // extension point, the code that "varies"
    // in LiskovBase and it's subclasses
    // called by the template method above
    // we expect it to be virtual and overridden
    public string getStuffString() {
        return "My name is Liskov";
    }
}

public class LiskovSub extends LiskovBase {
    // the extension overridden
    // the actual code that varied
    public string getStuffString() {
        return "I'm sub Liskov!";
    }
}
_

この例はOCPに準拠しており、馬鹿げているように見えますが、これは、処理するコードを増やすことで拡大されることを想像してください。私は、サブクラスがすべてを完全にオーバーライドし、オーバーライドされたコードが実装間でカットアンドペーストされるプロダクションにデプロイされたコードを見続けます。それは機能しますが、すべてのコードの複製と同様に、メンテナンスの悪夢のためのセットアップでもあります。

結論

これで、OCPとLSP、およびそれらの違い/類似性に関するいくつかの質問がすべて解決されることを願っています。同じようにそれらを却下するのは簡単ですが、上記の例はそうではないことを示すはずです。

上記のサンプルコードから収集したことに注意してください:

  • OCPは動作中のコードをロックダウンすることを目的としていますが、なんらかの拡張ポイントを使用して、コードを開いたままにしておきます。

    これは、テンプレートメソッドパターンの例のように変更されるコードをカプセル化することで、コードの重複を避けるためです。また、重大な変更は痛みを伴うため(つまり、1つの場所を変更し、他の場所でそれを壊す)、すぐに失敗する可能性もあります。メンテナンスのためには、変更が常に発生するため、変更をカプセル化するという概念は適切です。

  • LSPは、スーパータイプを実装するさまざまなオブジェクトを、ユーザーが実際のタイプを確認せずに処理できるようにすることです。これは本質的にpolymorphismが何についてであるかです。

    この原則は、型チェックと型変換を行うための代替手段を提供します。これは、型の数が増えるにつれて手に負えなくなり、プルアップリファクタリングやビジターなどのパターンの適用によって実現できます。

118
Spoike

これは多くの混乱を引き起こすものです。私はこれらの原則をいくらか哲学的に検討することを好みます。なぜなら、それらには多くの異なる例があり、時には具体的な例がそれらの本質全体を実際に捕捉しない場合があるからです。

OCPが修正しようとするもの

特定のプログラムに機能を追加する必要があるとします。特に手続き的に考えるように訓練された人々にとってそれを行う最も簡単な方法は、必要に応じてif節などを追加することです。

それに関する問題は

  1. 既存の機能するコードのフローを変更します。
  2. すべてのケースで新しい条件付き分岐を強制します。たとえば、書籍のリストがあり、それらの一部が販売中であり、それらすべてを繰り返して価格を印刷するとします。たとえば、販売されている場合、印刷された価格には文字列「 (発売中)"。

これを行うには、「is_on_sale」という名前のすべての書籍にフィールドを追加し、書籍の価格を印刷するときにそのフィールドを確認します。またはまたは 、別のタイプを使用してデータベースからセール中の本をインスタンス化できます。これは、価格文字列に「(ON SALE)」と印刷します(完璧なデザインではありませんが、ポイントを提供します)。

最初の手続き型ソリューションの問題は、各本の追加フィールドであり、多くの場合、余分な複雑さです。 2番目のソリューションは、実際に必要な場合にのみロジックを強制します。

ここで、さまざまなデータとロジックが必要となるケースがたくさんある可能性があるという事実を考えてみてください。クラスを設計するとき、または要件の変更に対応するときにOCPを念頭に置くことが良い考えであることがわかります。

ここまでで、主要なアイデアが得られるはずです。新しいコードを、手続き的な変更ではなく、ポリモーフィック拡張として実装できる状況に身を置いてみてください。

しかし、コンテキストを分析することを恐れないでください。そして、OCPなどの原則でさえ、慎重に扱わなければ20行のプログラムから20クラスの混乱を招く可能性があるため、欠点が利点を上回るかどうかを確認してください。

LSPが修正しようとするもの

私たちは皆、コードの再利用が大好きです。続く病気は、多くのプログラムがそれを完全に理解していないことであり、コードの数行を除いて、一般的なコード行を盲目的に因数分解してモジュール間の冗長な密結合を作成し、実施すべき概念的な作業に関する限り、共通点はありません。

この最大の例はインターフェースの再利用です。あなたはおそらくそれを自分で目撃したでしょう。クラスがインターフェースを実装するのは、その論理的な実装(または具体的な基本クラスの場合は拡張)のためではなく、その時点でたまたま宣言するメソッドが関係する限り適切なシグネチャを持っているためです。

しかし、その後、問題が発生します。クラスが宣言するメソッドのシグネチャのみを考慮してインターフェイスを実装する場合、クラスのインスタンスを1つの概念的な機能から完全に異なる機能を必要とする場所に渡すことができ、類似のシグネチャのみに依存することになります。

それは恐ろしいことではありませんが、それは多くの混乱を引き起こします、そして私たちは自分たちがこのような間違いを犯すことを防ぐ技術を持っています。インターフェイスをAPI + Protocolとして扱う必要があります。 APIは宣言で明らかであり、プロトコルはインターフェースの既存の使用で明らかです。同じAPIを共有する2つの概念的なプロトコルがある場合、それらは2つの異なるインターフェースとして表す必要があります。そうしないと、DRY=独断主義に巻き込まれ、皮肉なことに、コードの保守が難しくなるだけです。

これで、定義を完全に理解できるはずです。 LSPは言う:基本クラスから継承せず、基本クラスに依存する他の場所がうまくいかないサブクラスに機能を実装しないでください。

15
Yam Marcovic

私の理解から:

OCPによると「新しい関数を追加する場合は、既存のクラスを変更するのではなく、既存のクラスを拡張する新しいクラスを作成してください。」

LSPは言う:「既存のクラスを拡張する新しいクラスを作成する場合、それがそのベースと完全に交換可能であることを確認してください。」

だから私はそれらがお互いを補完すると思いますが、それらは等しくありません。

8
henginy

OCPとLSPの両方が変更に関連していることは事実ですが、OCPが説明する変更の種類は、LSPが説明するものではありません。

OCPに関する変更は、既存のクラスで開発者コードを書くの物理的なアクションです。

LSPは、派生クラスがその基本クラスと比較してもたらす動作変更、およびスーパークラスの代わりにサブクラスを使用することによって引き起こされる可能性があるプログラムの実行のruntime変更を扱います。

したがって、それらは遠くから同じように見えるかもしれませんが、OCP!= LSPです。実際、私はそれらが唯一の2 SOLID=相互に理解できない原則であるかもしれないと思う。

4
guillaume31

簡単な言葉でLSPは、Fooの任意のインスタンスを、プログラム機能を失うことなく、Fooから派生したBarの任意のインスタンスに置き換えることができると述べています。

これは間違っています。 LSPは、BarクラスがFooから派生している場合、コードがFooを使用する場合は予期されない動作をクラスBarが導入すべきではないと述べています。機能の喪失とは関係ありません。機能を削除できますが、Fooを使用するコードがこの機能に依存していない場合のみです。

しかし、結局のところ、ほとんどの場合、Fooを使用するコードはそのすべての動作に依存しているため、これを達成することは困難です。したがって、それを削除するとLSPに違反します。ただし、このように単純化することはLSPの一部にすぎません。

2
Euphoric

LSPとOCPは同じではありません。

LSPはプログラムの正しさについて話します現状のまま。サブタイプのインスタンスが祖先タイプのコードに代入されたときにプログラムの正確性を損なう場合、LSPの違反を示したことになります。これを表示するにはテストをモックアップする必要があるかもしれませんが、基礎となるコードベースを変更する必要はありません。 プログラム自体を検証して、LSPに適合しているかどうかを確認しています。

OCPは、プログラムコード内のchangesの正確さ、あるソースバージョンから別のバージョンへの差分について話します。動作は変更しないでください。拡張するだけです。典型的な例はフィールド追加です。すべての既存のフィールドは以前と同様に動作し続けます。新しいフィールドは機能を追加するだけです。ただし、フィールドを削除すると、通常OCPに違反します。ここでは、プログラムバージョンデルタを検証して、OCPに適合しているかどうかを確認しています。

これがLSPとOCPの主な違いです。前者はそのままのコードベースのみを検証し、後者はあるバージョンから次のバージョンへのコードベースデルタのみを検証します。そのため、それらは同じものにすることはできず、異なるものを検証するdefinedです。

より正式な証明を提供します。「LSPはOCPを暗示する」とは、デルタを意味します(OCPは些細な場合以外のものを必要とするため)が、LSPはそれを要求しません。したがって、それは明らかに誤りです。逆に、OCPはデルタに関するステートメントであるため、「OCPはLSPを暗示する」と反論することができます。したがって、プログラムインプレースプログラムに関するステートメントについては何も述べていません。これは、任意のプログラムを使用して、任意のデルタを作成できるという事実に基づいています。彼らは完全に独立しています。

0
Brad Thomas

違反する可能性のあるオブジェクトについて

違いを理解するには、両方の原則の主題を理解する必要があります。これは、コードや状況の抽象的な部分ではなく、何らかの原則に違反しているわけではありません。 OCPまたはLSPに違反する可能性があるのは、常に特定のコンポーネント(関数、クラス、またはモジュール)です。

LSPに違反する可能性のある人

LSPが壊れているかどうかを確認できるのは、何らかのコントラクトを持つインターフェースとそのインターフェースの実装がある場合のみです。実装がインターフェイス、または一般的に言えばコントラクトに準拠していない場合、LSPは機能しません。

最も単純な例:

_class Container {
    // Should add the object to the container.
    void addObject(object) {
        internalArray.append(object);
    }

    int size() {
        return internalArray.size();
    }
}

class CustomContainer extends Container {
    @Override void addObject(object) {
        System.console.print("Skipping object! Ha-ha!");
    }
}

void fillWithRandomNumbers(Container container) {
    while (container.size() < 42) {
        container.addObject(Randomizer.getNumber())
    }
}
_

契約は、addObjectがその引数をコンテナに追加する必要があることを明確に述べています。そしてCustomContainerは明らかにその契約を破ります。したがって、_CustomContainer.addObject_関数はLSPに違反しています。したがって、CustomContainerクラスはLSPに違反しています。最も重要な結果は、CustomContainerfillWithRandomNumbers()に渡すことができないことです。 ContainerCustomContainerで置き換えることはできません。

非常に重要なポイントを覚えておいてください。 LSPを破壊するのはこのコード全体ではなく、具体的には_CustomContainer.addObject_であり、LSPを破壊するのは通常CustomContainerです。 LSPに違反していると述べるときは、常に次の2つを指定する必要があります。

  • LSPに違反するエンティティ。
  • エンティティによって破られた契約。

それでおしまい。ただの契約とその実装。コードのダウンキャストは、LSP違反について何も述べていません。

OCPに違反する可能性のある人

限られたデータセットとそのデータセットの値を処理するコンポーネントがある場合にのみ、OCPに違反しているかどうかを確認できます。データセットの制限が時間とともに変化する可能性があり、そのためにコンポーネントのソースコードを変更する必要がある場合、コンポーネントはOCPに違反しています。

複雑に聞こえます。簡単な例を試してみましょう:

_enum Platform {
    iOS,
    Android
}

class PlatformDescriber {
    String describe(Platform platform) {
        switch (platform) {
            case iOS: return "iPhone OS, v10.0.1";
            case Android: return "Android, v7.1";
        }
    }
}
_

データセットは、サポートされているプラ​​ットフォームのセットです。 PlatformDescriberは、そのデータセットの値を処理するコンポーネントです。新しいプラットフォームを追加するには、PlatformDescriberのソースコードを更新する必要があります。したがって、PlatformDescriberクラスはOCPに違反しています。

もう一つの例:

_class Shop {
    void sellItemToCustomer(item, customer) {
        // some buisiness logic here
        ...
        logger.logItemSold()
    }
}

class Logger {
    void logItemSold() {
        logger.logToStdErr("an item was sold")
        logger.logToRemote("an item was sold")
        logger.logToDatabase("an item was sold")
    }
}
_

「データセット」は、ログエントリを追加する必要があるチャネルのセットです。 Loggerは、すべてのチャネルにエントリを追加するコンポーネントです。ログ記録の別の方法のサポートを追加するには、Loggerのソースコードを更新する必要があります。したがって、LoggerクラスはOCPに違反しています。

どちらの例でも、データセットは意味的に修正されたものではないことに注意してください。時間の経過とともに変化する可能性があります。新しいプラットフォームが登場するかもしれません。ロギングの新しいチャネルが出現する可能性があります。そのときにコンポーネントを更新する必要がある場合、OCPに違反します。

限界を押し上げる

ここでトリッキーな部分です。上記の例を次の例と比較してください。

_enum GregorianWeekDay {
    Monday,
    Tuesday,
    Wednesday,
    Thursday,
    Friday,
    Saturday,
    Sunday
}

String translateToRussian(GregorianWeekDay weekDay) {
    switch (weekDay) {
        case Monday: return "Понедельник";
        case Tuesday: return "Вторник";
        case Wednesday: return "Среда";
        case Thursday: return "Четверг";
        case Friday: return "Пятница";
        case Saturday: return "Суббота";
        case Sunday: return "Воскресенье";
    }
}
_

translateToRussianはOCPに違反していると思うかもしれません。しかし、実際にはそうではありません。 GregorianWeekDayには、正確な名前で正確に7週間という特定の制限があります。そして重要なことは、これらの制限は時間の経過とともに意味的に変更できないということです。グレゴリオ暦の週は常に7日あります。月曜日、火曜日などが常にあります。このデータセットは意味的に固定されています。 translateToRussianのソースコードを変更する必要はありません。したがって、OCPには違反しません。

switchステートメントの枯渇が、必ずしもOCPの破損を示しているとは限らないことは明らかです。

違い

違いを感じてください:

  • LSPの主題は、「インターフェース/契約の実装」です。実装がコントラクトに準拠していない場合、LSPに違反します。拡張可能かどうかにかかわらず、その実装が時間の経過とともに変化するかどうかは重要ではありません。
  • OCPの主題は、「要件の変更に対応する方法」です。新しいタイプのデータをサポートするために、そのデータを処理するコンポーネントのソースコードを変更する必要がある場合、そのコンポーネントはOCPを壊します。コンポーネントが契約に違反しているかどうかは重要ではありません。

これらの条件は完全に直交しています。

@ Spoikeの答え1つの原則に違反し、他の原則に違反している部分は完全に間違っています。

最初の例では、for- loop部分は、変更しないと拡張できないため、明らかにOCPに違反しています。ただし、LSP違反の兆候はありません。そして、ContextコントラクトがgetPersonsにBossまたはPeon以外のものを返すことを許可するかどうかさえも明確ではありません。 IPersonサブクラスを返すことを許可するコントラクトを想定しても、この事後条件をオーバーライドして違反するクラスはありません。さらに、getPersonsが3番目のクラスのインスタンスを返す場合、for- loopは失敗することなくその仕事をします。しかし、その事実はLSPとは何の関係もありません。

次。 2番目の例では、LSPも​​OCPも違反していません。繰り返しますが、Contextの部分はLSPとは何の関係もありません。定義されたコントラクト、サブクラス、オーバーライドの破棄はありません。 LSPに従う必要があるのはContextではなく、LiskovSubはベースの契約を破るべきではありません。 OCPに関して、クラスは本当に閉じていますか?-はい、そうです。拡張するための変更は必要ありません。明らかに、拡張ポイントの名前は制限なしで何でもやりたいことを示します。この例は実際にはあまり役に立ちませんが、明らかにOCPに違反していません。

OCPまたはLSPの真の違反を含むいくつかの正しい例を作ってみましょう。

LSPではなくOCPをフォローする

_interface Platform {
    String name();
    String version();
}

class iOS implements Platform {
    @Override String name() { return "iOS"; }
    @Override String version() { return "10.0.1"; }
}

interface PlatformSerializer {
    String toJson(Platform platform);
}

class HumanReadablePlatformSerializer implements PlatformSerializer {
    String toJson(Platform platform) {
        return platform.name() + ", v" + platform.version();
    }
}
_

ここで、HumanReadablePlatformSerializerは、新しいプラットフォームが追加されたときに変更を必要としません。したがって、OCPに従います。

ただし、契約ではtoJsonが適切にフォーマットされたJSONを返す必要があります。クラスはそれをしません。そのため、PlatformSerializerを使用してネットワーク要求の本文をフォーマットするコンポーネントに渡すことはできません。したがって、HumanReadablePlatformSerializerはLSPに違反しています。

LSPをフォローするがOCPはフォローしない

前の例のいくつかの変更:

_class Android implements Platform {
    @Override String name() { return "Android"; }
    @Override String version() { return "7.1"; }
}
class HumanReadablePlatformSerializer implements PlatformSerializer {
    String toJson(Platform platform) {
        return "{ "
                + "\"name\": \"" + platform.name() + "\","
                + "\"version\": \"" + platform.version() + "\","
                + "\"most-popular\": " + isMostPopular(platform) + ","
                + "}"
    }

    boolean isMostPopular(Platform platform) {
        return (platform instanceof Android)
    }
}
_

シリアライザは正しくフォーマットされたJSON文字列を返します。したがって、ここではLSP違反はありません。

ただし、プラットフォームが最も広く使用されている場合は、JSONに対応する表示が必要であるという要件があります。この例では、いつかiOSが最も人気のあるプラットフォームになるため、OCPは_HumanReadablePlatformSerializer.isMostPopular_関数に違反しています。正式には、現在最も使用されているプラ​​ットフォームのセットが「Android」として定義されており、isMostPopularがそのデータセットを適切に処理していないことを意味します。データセットは意味的に固定されておらず、時間の経過とともに自由に変化する可能性があります。 HumanReadablePlatformSerializerのソースコードは、変更された場合に更新する必要があります。

この例では、単一責任の違反にも気付く場合があります。同じ対象エンティティで両方の原則を実証できるように意図的に作成しました。 SRPを修正するには、isMostPopular関数を外部Helperに抽出し、_PlatformSerializer.toJson_にパラメーターを追加します。しかし、それは別の話です。

0
mekarthedev