LSP vs OCP / Liskov Substitution VS Open Close


48

私は、OOPの固い原則を理解しようとしていますが、LSPとOCPにはいくつかの類似点があるという結論に達しました(詳しくは言いませんが)。

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

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

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

私が間違っている場合は修正してください。これらのアイデアを本当に理解したいです。答えてくれてありがとう。


4
これは両方の概念の非常に狭い解釈です。オープン/クローズは維持できますが、LSPに違反しています。Rectangle / SquareまたはEllipse / Circleの例は良い例です。どちらもOCPに準拠していますが、どちらもLSPに違反しています。
ジョエルイーサートン

1
世界(または少なくともインターネット)はこれについて混乱しています。kirkk.com/modularity/2009/12/solid-principles-of-class-design。この男は、LSPの違反はOCPの違反でもあると言います。そして、156ページの「ソフトウェアエンジニアリングデザイン:理論と実践」の本で、著者はOCPに準拠しているがLSPに違反している例を示しています。私はこれをあきらめました。
マノジR

@JoelEthertonこれらのペアは、変更可能な場合にのみLSPに違反します。不変の場合、派生はLSPに違反SquareRectangleません。(あなたは、正方形持つことができるので、しかし、それは不変の場合には、まだおそらく悪いデザインだRectangleではありませんsのSquare数学と一致しないが)
CodesInChaos

単純なアナロジー(ライブラリライターユーザーの観点から)。LSPは、(インターフェイスまたはユーザーマニュアルで)言うことの100%を実装すると主張する製品(ライブラリ)を販売するようなものですが、実際にはそうではありません(または言われていることと一致しません)。OCPは、製品(ライブラリ)を販売するようなもので、新しい機能(ファームウェアなど)が出たときにアップグレード(拡張)できるという約束がありますが、実際には工場サービスなしではアップグレードできません。
rwong

回答:


119

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

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

開閉原理(OCP)

ボブおじさんによると:

クラスの動作を変更せずに拡張できる必要があります。

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

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

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

ここで、原理がソフトウェア実体に適用されるのは、より明確です。悪い例は、拡張ポイントを提供する代わりにコードを完全に変更しているため、ソフトウェアエンティティをオーバーライドすることです。ソフトウェアエンティティ自体の動作は拡張可能である必要があります。これの良い例は、Strategyパターンの実装です(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();
}

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

つまり、コンテキストクラスは変更のために閉じられますが、拡張のために開かれます。実際には、継承の代わりにオブジェクト合成を使用して動作を設定しているため、別の基本原則に従います。

「「クラス継承」よりも「オブジェクト構成」を優先します。」(ギャングオブフォー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を実行するには、サブクラス化以外の方法もあります。一つの方法は、ミックスインを使用してクラスを拡張用に開いたままにすることです。これは、たとえばクラスベースではなくプロトタイプベースの言語で役立ちます。アイデアは、必要に応じてより多くのメソッドまたは属性を持つ動的オブジェクト、つまり他のオブジェクトとブレンドまたは「混合」するオブジェクトを修正することです。

アンカー用のシンプルな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>

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

OCPに関しては、「ミックスイン」は拡張機能です。上記の例YoutubeLinkでは、変更のために閉じられていますが、ミックスインを使用して拡張のために開いているソフトウェアエンティティです。オブジェクト階層がフラット化されているため、型を確認できません。しかし、これは本当に悪いことではありません。タイプをチェックすることは一般に悪い考えであり、ポリモーフィズムで考えを壊すことをさらに下で説明します。

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

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

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

リスコフの置換原理(LSP)

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

派生クラスは、基本クラスに代用可能でなければなりません。

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

何ここで望まれることは次の置換プロパティのようなものである:各オブジェクトの場合はo1タイプのSオブジェクトがあるo2タイプのは、Tすべてのプログラムのために、このようなことPの用語で定義されたT、の動作はP時に変更されていないo1ために置換されo2、その後SのサブタイプですT

クラスについてはまったく言及していませんので、しばらくこれを肩代わりしてください。JavaScriptでは、明示的にクラスベースではありませんが、実際にはLSPに従うことができます。プログラムに、少なくとも2つのJavaScriptオブジェクトのリストがある場合:

  • 同じ方法で計算する必要があり、
  • 同じ動作をする
  • それ以外は何らかの点で完全に異なります

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

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

さて、それはすべての時間間違っているではないかもしれませんが、あなたには、いくつかの実行する衝動がある場合はその型チェックinstanceofもう少しそれが必要以上に自分自身のためのコンボリューションまたは列挙型を、あなたがプログラムを実行される可能性があります。しかし、これは常にそうではありません。解決策が十分に小さい場合、物事を機能させるための迅速で汚いハックは私の心に許す譲歩であり、容赦のないリファクタリングを実践すれば、変更が要求されると改善されるかもしれません。

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

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

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

ビジターパターンは、大きな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");
    }
}

この時点で、プログラマがVisitorパターンを知らなかった場合、代わりに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つの原則に違反するが、他の原則に従う

原則の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();
    }
}

型チェックと型変換を行う必要があります!上記でどのように型チェックが悪いことであるかを覚えていますか?大野!ただし、上でも述べたように、プルアップリファクタリングを行うか、Visitorパターンを実装することを恐れないでください。この場合、一般的なメソッドを追加した後、プルアップリファクタリングを実行できます。

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();
}

OSPではなくLSPに従います

OSPではなくLSPに続くいくつかのコードを見てみましょう、それはちょっと不自然ですが、この1つに非常に微妙な間違いがあります:

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に従うと思いますが、よく見ると、クラスは本当に閉じているのでしょうか?doStuffメソッドが単に行を印刷するだけではない場合はどうなりますか?

OCPに続く場合の答えは単純です:NO。これは、このオブジェクト設計では、コードを他のもので完全にオーバーライドする必要があるためではありません。これにより、基本クラスからコードをコピーして動作させる必要があるため、カットアンドペーストワームの缶が開きます。doStuffこの方法は、確か拡張のために開いているが、それは完全に変更のため閉鎖されていませんでした。

これにTemplateメソッドパターンを適用できます。テンプレートメソッドパターンはフレームワークで非常に一般的であるため、知らないうちに使用していた可能性があります(たとえば、java swingコンポーネント、c#フォームおよびコンポーネントなど)。doStuff変更のためにメソッドを閉じ、javaのfinalキーワードでマークすることでメソッドが閉じられたままになるようにする方法の1つです。このキーワードは、誰もクラスをさらにサブクラス化することを防ぎます(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は、実際の型を確認せずに、スーパータイプを実装するさまざまなオブジェクトをユーザーが処理できるようにすることです。これは本質的にポリモーフィズムの目的です。

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


7
これは、常に継承による実装を意味するという意味でOCPを単純化しすぎないため、良い説明です。OCPとSRPを一部の人々の心の中で結合するのは、実際には2つの完全に別個の概念である可能性があるのに、その単純化です。
エリックキング

5
これは、これまで見た中で最高のスタック交換の回答の1つです。私はそれを10回賛成できたらいいのにと思います。よくやった、そして素晴らしい説明をありがとう。
ボブホーン

そこで、クラスベースのプログラミング言語ではありませんが、LSPに引き続き従うことができるJavascriptに宣伝文句を追加して、テキストを流soに読めるように編集しました。ふう!
スポーク

LSPのボブおじさんからの引用は正しい(彼のWebサイトと同じ)のですが、逆ではないでしょうか?「基底クラスは、派生クラスの代わりに使用できるようにすべき」と述べるべきではありませんか?LSPでは、「互換性」のテストは、基本クラスではなく派生クラスに対して行われます。それでも、私は英語を母国語とする人ではないので、見逃しているかもしれないフレーズに関する詳細があるかもしれません。
アルファ

@Alpha:それはいい質問です。基本クラスは、常に派生クラスで代用可能です。そうでない場合、継承は機能しません。実装する必要のある拡張クラスからメンバー(メソッドまたは属性/フィールド)を除外すると、コンパイラー(少なくともJavaとC#で)は文句を言います。LSPは、それらの派生クラスのユーザーがそれらについて知る必要があるため、派生クラスでローカルでのみ使用可能なメソッドを追加しないようにすることを目的としています。コードが大きくなると、そのようなメソッドを維持するのが難しくなります。
スポーク

15

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

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

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

それに関する問題は

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

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

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

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

ここまでで、主なアイデアが得られるはずです。新しいコードが手続き型の修正ではなく、ポリモーフィックな拡張機能として実装できる状況に身を置くようにしてください。

ただし、OCPなどの原則でさえ、慎重に扱わなければ20行のプログラムから20クラスの混乱を引き起こす可能性があるため、コンテキストを分析し、欠点が利益を上回るかどうかを恐れることはありません

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

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

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

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

それはそれほど恐ろしいことではありませんが、それは多くの混乱を引き起こします。行う必要があるのは、インターフェイスをAPI +プロトコルとして扱うことです。APIは宣言で明らかであり、プロトコルはインターフェースの既存の使用で明らかです。同じAPIを共有する2つの概念プロトコルがある場合、それらは2つの異なるインターフェースとして表される必要があります。そうでなければ、DRYの独断に巻き込まれ、皮肉なことに、コードの保守が難しくなります。

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


1
私はこれとSpoikeの答えに賛成票を投じることができるようにサインアップしました。素晴らしい仕事です。
デビッドカルプ

7

私の理解から:

OCPは次のよ​​うに述べています。「新しい機能を追加する場合は、既存のクラスを変更するのではなく、既存のクラスを拡張する新しいクラスを作成します。」

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

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


4

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

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

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

したがって、OCP!= LSPの距離からは似ているように見えますが。実際、それらは互いの観点から理解できない唯一の2つの固い原則かもしれないと思います。


2

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

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

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


非常に一般的なケースは、置換されたオブジェクトが副作用を取り除く場合です。何も出力しないダミーロガー、またはテストで使用されるモックオブジェクト。
役に立たない

0

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

違いを理解するには、両方の原則の主題を理解する必要があります。コードに違反したり、原則に反する可能性があるのは、コードや状況の抽象的な部分ではありません。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に違反します。最も重要な結果は、CustomContainerに渡すことができないことfillWithRandomNumbers()です。Containerで置き換えることはできませんCustomContainer

非常に重要な点に留意してください。LSPを破壊するのはこのコード全体ではなく、具体的CustomContainer.addObjectかつ一般的CustomContainerにLSPを破壊するのです。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 "Воскресенье";
    }
}

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

これで、使い果たされたswitchステートメントが常に壊れたOCPの兆候ではないことは明らかです。

違い

違いを感じてください:

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

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

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

最初の例では、for-loop部分はOCPに明らかに違反しています。これは、変更しないと拡張できないためです。しかし、LSP違反の兆候はありません。IFおよびそれも明らかではないが、Context契約がgetPersonsを除いて何かを返すことができますBossPeonIPersonサブクラスを返すことを許可するコントラクトを想定しても、この事後条件をオーバーライドして違反するクラスはありません。さらに、getPersonsが3番目のクラスのインスタンスを返す場合、-loopはfor失敗せずにそのジョブを実行します。しかし、その事実はLSPとは関係ありません。

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

OCPまたはLSPに真に違反する正しい例をいくつか作ってみましょう。

LCPではなく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、ネットワーク要求の本文をフォーマットするために使用するコンポーネントに渡すことはできません。したがって、HumanReadablePlatformSerializerLSPに違反します。

OCPではなくLSPをフォロー

前の例に対するいくつかの変更:

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に対応する指示が必要であるという要件があります。この例では、HumanReadablePlatformSerializer.isMostPopulariOSがいつか最も人気のあるプラットフォームになるため、OCPは機能に違反しています。正式には、最も使用されているプラ​​ットフォームのセットは、現時点では「Android」として定義されており、isMostPopularそのデータセットを不適切に処理することを意味します。データセットは意味的に固定されておらず、時間の経過とともに自由に変更される可能性があります。HumanReadablePlatformSerializerのソースコードは、変更の場合に更新する必要があります。

この例では、単一責任の違反に気付くかもしれません。同じテーマで両方の原則を実証できるように、意図的に作成しました。SRPを修正するには、isMostPopular関数を外部に抽出しHelper、パラメータをに追加しますPlatformSerializer.toJson。しかし、それは別の話です。


0

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

LSPは、プログラムの正しさについて語っています。サブタイプのインスタンスが祖先タイプのコードに置き換えられたときにプログラムの正確性を損なう場合、LSP違反を示しています。これを示すためにテストを模擬する必要があるかもしれませんが、基礎となるコードベースを変更する必要はありません。プログラム自体を検証して、LSPを満たしているかどうかを確認しています。

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

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

「LSPはOCPを意味します」と言うことは、デルタを意味します(OCPは些細な場合以外を必要とするため)が、LSPはそれを必要としません。それは明らかに間違っています。逆に、OCPはデルタに関するステートメントであると言うだけで、「OCPはLSPを意味する」と反証することができます。したがって、インプレースプログラムに関するステートメントについては何も言いません。これは、任意のプログラムを開始して、任意のデルタを作成できるという事実から得られます。彼らは完全に独立しています。


-1

私はクライアントの観点からそれを見ます。クライアントがインターフェイスの機能を使用しており、その機能がクラスAによって内部的に実装されている場合、クラスAを拡張するクラスBがあり、そのインターフェイスからクラスAを削除してクラスBを配置すると、クラスBはクライアントにも同じ機能を提供します。標準的な例は、泳ぐDuckクラスです。ToyDuckがDuckを拡張する場合、泳ぐ必要があり、泳げないと文句を言うこともありません。そうでない場合、ToyDuckはDuckクラスを拡張しません。


人々が答えを投票する間もコメントを入れると、非常に建設的です。結局のところ、私たちは皆知識を共有するためにここにいるのであり、適切な理由なしに単に判断を下すだけでは何の目的にもなりません。
AKS

これは作られたポイントを超える大幅な提供の何にも思えるし、前6つの回答で説明していません
ブヨ

1
原則の1つであるLを説明しているようです。それは大丈夫ですが、質問は2つの異なる原則の比較/対比を求めました。それがおそらく誰かがそれを支持しなかった理由です。
StarWeaver 14
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.