質問

リスコフ置換原則 (LSP) はオブジェクト指向設計の基本原則であると聞いたことがあります。それは何ですか?またその使用例は何ですか?

役に立ちましたか?

解決 2

リスコフ置換原理 (LSP、 ) は、オブジェクト指向プログラミングの概念であり、次のように述べられています。

ポインターまたは基本クラスへの参照を使用する関数は、派生クラスのオブジェクトを知らずに使用できる必要があります。

LSP の核心は、インターフェイスとコントラクトだけでなく、クラスとクラスをいつ拡張するかを決定する方法に関するものです。目標を達成するには、構図などの別の戦略を使用してください。

この点を説明するために私が見た中で最も効果的な方法は、 ヘッドファースト OOA&D. 。これらは、あなたが戦略ゲームのフレームワークを構築するプロジェクトの開発者であるというシナリオを示しています。

これらは、次のようなボードを表すクラスを提示します。

Class Diagram

すべてのメソッドは、X 座標と Y 座標をパラメータとして受け取り、次の 2 次元配列内のタイル位置を特定します。 Tiles. 。これにより、ゲーム開発者はゲームの進行中にボード内のユニットを管理できるようになります。

この本はさらに要件を変更して、飛行のあるゲームに対応するにはゲーム フレームワークも 3D ゲーム ボードをサポートする必要があると述べています。それで、 ThreeDBoard を拡張するクラスが導入されました Board.

一見すると、これは良い決断のように思えます。 Board 両方を提供します Height そして Width プロパティと ThreeDBoard Z軸を提供します。

それが壊れるのは、から継承された他のすべてのメンバーを見るときです。 Board. 。の方法 AddUnit, GetTile, GetUnits など、すべてが X パラメータと Y パラメータの両方を受け取ります。 Board クラスですが、 ThreeDBoard Zパラメータも必要です。

したがって、Z パラメータを使用してこれらのメソッドを再度実装する必要があります。Z パラメータにはコンテキストがありません。 Board クラスとそこから継承されたメソッド Board クラスは意味を失います。を使用しようとするコードの単位 ThreeDBoard クラスを基本クラスとして Board 非常に運が悪いでしょう。

別のアプローチを見つけたほうがいいかもしれません。延長する代わりに Board, ThreeDBoard で構成される必要があります Board オブジェクト。1つ Board Z 軸の単位あたりのオブジェクト。

これにより、カプセル化や再利用などの優れたオブジェクト指向原則を使用できるようになり、LSP に違反することはありません。

他のヒント

LSP を説明する素晴らしい例 (最近聞いたポッドキャストでボブおじさんが挙げたもの) は、自然言語では適切に聞こえるものが、コードでは正しく動作しない場合があるということでした。

数学では、 Square です Rectangle. 。確かに、それは長方形の特殊化です。「is a」を使用すると、これを継承でモデル化したくなります。ただし、コードで作成した場合は、 Square から派生する Rectangle, 、次に Square どこでも使えるはずです Rectangle. 。これにより、奇妙な動作が発生します。

あなたが持っていたと想像してください SetWidth そして SetHeight あなたのメソッド Rectangle 基本クラス;これは完全に論理的だと思われます。ただし、あなたの場合は、 Rectangle を指す参照 Square, 、 それから SetWidth そして SetHeight 一方を設定すると、それに合わせてもう一方も変更されるため、意味がありません。この場合 Square リスコフ置換テストに失敗しました Rectangle そして持つことの抽象化 Square から継承する Rectangle 悪いものです。

enter image description here

他の貴重なアイテムもチェックしてみてください SOLID Principles やる気を起こさせるポスター.

LSP は不変条件に関係します。

典型的な例は、次の疑似コード宣言によって示されます (実装は省略されています)。

class Rectangle {
    int getHeight()
    void setHeight(int value)
    int getWidth()
    void setWidth(int value)
}

class Square : Rectangle { }

インターフェイスは一致していますが、問題が発生しました。その理由は、正方形と長方形の数学的定義から生じる不変条件に違反しているためです。ゲッターとセッターの仕組みは、 Rectangle は次の不変条件を満たす必要があります。

void invariant(Rectangle r) {
    r.setHeight(200)
    r.setWidth(100)
    assert(r.getHeight() == 200 and r.getWidth() == 100)
}

ただし、この不変条件は、 しなければならない ~の正しい実装によって違反される Square, したがって、これは有効な代替品ではありません。 Rectangle.

代替可能性とは、コンピュータ プログラム内で S が T のサブタイプである場合、T 型のオブジェクトを S 型のオブジェクトで置き換えることができるというオブジェクト指向プログラミングの原則です。

Java で簡単な例を実行してみましょう。

悪い例

public class Bird{
    public void fly(){}
}
public class Duck extends Bird{}

アヒルは鳥なので飛べますが、これはどうでしょうか。

public class Ostrich extends Bird{}

ダチョウは鳥ですが、飛ぶことができません。ダチョウクラスは鳥クラスのサブタイプですが、飛ぶメソッドを使用できません。これは、LSP 原則に違反していることを意味します。

良い例え

public class Bird{
}
public class FlyingBirds extends Bird{
    public void fly(){}
}
public class Duck extends FlyingBirds{}
public class Ostrich extends Bird{} 

ロバート・マーティンは素晴らしい リスコフ置換原理に関する論文. 。原則が違反される可能性のある微妙な方法とそれほど微妙ではない方法について説明します。

論文の関連部分の一部 (2 番目の例は大幅に要約されていることに注意してください)。

LSP 違反の簡単な例

この原則の最も明白な違反の1つは、オブジェクトのタイプに基づいて関数を選択するためのC ++ランタイムタイプ情報(RTTI)を使用することです。つまり:

void DrawShape(const Shape& s)
{
  if (typeid(s) == typeid(Square))
    DrawSquare(static_cast<Square&>(s)); 
  else if (typeid(s) == typeid(Circle))
    DrawCircle(static_cast<Circle&>(s));
}

明らかに、 DrawShape 機能が正しく形成されていません。それは、のすべての可能な派生物について知っておく必要があります Shape クラス。 Shape が作成されます。実際、多くの人がこの関数の構造をオブジェクト指向設計にとって忌まわしいものとみなしています。

正方形と長方形、より微妙な違反。

ただし、LSP に違反する、より巧妙な方法は他にもあります。を使用するアプリケーションを考えてみましょう。 Rectangle 以下に説明するクラス:

class Rectangle
{
  public:
    void SetWidth(double w) {itsWidth=w;}
    void SetHeight(double h) {itsHeight=w;}
    double GetHeight() const {return itsHeight;}
    double GetWidth() const {return itsWidth;}
  private:
    double itsWidth;
    double itsHeight;
};

...]いつかユーザーが長方形に加えて正方形を操作する能力を要求すると想像してください。[...]

明らかに、正方形は通常の意図や目的からすれば長方形です。ISA の関係が成り立つため、以下をモデル化するのは論理的です。 Squareから派生したクラス Rectangle. [...]

Square を継承します SetWidth そして SetHeight 機能。これらの関数は、aに対して完全に不適切です Square, 、正方形の幅と高さは同一であるため。これは、デザインに問題があるという重要な手がかりになるはずです。ただし、問題を回避する方法があります。オーバーライドできる SetWidth そして SetHeight [...]

ただし、次の関数を考えてみましょう。

void f(Rectangle& r)
{
  r.SetWidth(32); // calls Rectangle::SetWidth
}

への参照を渡すと、 Square オブジェクトをこの関数に追加すると、 Square 高さは変更されないため、オブジェクトは破損します。これは明らかな LSP 違反です。この関数は、その議論の派生物に対しては機能しません。

[...]

LSP は、一部のコードが特定の型のメソッドを呼び出していると考えられる場合に必要です。 T, 、そして無意識のうちにある型のメソッドを呼び出す可能性があります。 S, 、 どこ S extends T (すなわち、 S スーパータイプを継承する、スーパータイプから派生する、またはスーパータイプのサブタイプである T).

たとえば、これは、次のタイプの入力パラメータを持つ関数の場合に発生します。 T, 、と呼ばれます(すなわち、type の引数値を使用して呼び出されます) S. 。または、タイプの識別子が T, 、 type の値が割り当てられます。 S.

val id : T = new S() // id thinks it's a T, but is a S

LSP には期待値が必要です (つまり、不変式) 型のメソッドの場合 T (例えば。 Rectangle)、次のタイプのメソッドの場合は違反されません。 S (例えば。 Square) が代わりに呼び出されます。

val rect : Rectangle = new Square(5) // thinks it's a Rectangle, but is a Square
val rect2 : Rectangle = rect.setWidth(10) // height is 10, LSP violation

が付いたタイプでも、 不変フィールド 不変条件がまだ残っています。例:の 不変 長方形セッターは寸法が個別に変更されることを期待していますが、 不変 スクエアセッターはこの期待に反します。

class Rectangle( val width : Int, val height : Int )
{
   def setWidth( w : Int ) = new Rectangle(w, height)
   def setHeight( h : Int ) = new Rectangle(width, h)
}

class Square( val side : Int ) extends Rectangle(side, side)
{
   override def setWidth( s : Int ) = new Square(s)
   override def setHeight( s : Int ) = new Square(s)
}

LSP では、サブタイプの各メソッドが必要です。 S 反変の入力パラメータと共変の出力が必要です。

反変とは、分散が継承の方向に逆であることを意味します。タイプ Si, 、サブタイプの各メソッドの各入力パラメータの S, 、同じであるか、 スーパータイプ タイプの Ti スーパータイプの対応するメソッドの対応する入力パラメータの T.

共分散とは、分散が継承の同じ方向にあることを意味します。タイプ So, 、サブタイプの各メソッドの出力 S, 、同じであるか、 サブタイプ タイプの To スーパータイプの対応するメソッドの対応する出力の T.

これは、呼び出し側が型があると考えている場合に発生するためです。 T, 、のメソッドを呼び出していると考えられます。 T, の場合、次の型の引数を指定します。 Ti そして出力をタイプに割り当てます To. 。実際に対応するメソッドを呼び出しているとき S, 、その後、それぞれ Ti 入力引数はに割り当てられます Si 入力パラメータ、および So 出力はタイプに割り当てられます To. 。したがって、もし Si 反変ではありませんでしたに Ti, 、次にサブタイプ Xi—これはサブタイプではありません Si—に割り当てられる可能性があります Ti.

さらに、言語の場合 (例:Scala または Ceylon) は、型多態性パラメーター (つまり、ジェネリックス)、型の各型パラメーターの分散アノテーションの同方向または逆方向 T でなければなりません 反対 (すべてのメソッドの) すべての入力パラメータまたは出力に対して、それぞれ同じ方向または同じ方向 T) 型パラメータの型を持ちます。

さらに、関数タイプを持つ入力パラメーターまたは出力ごとに、必要な分散の方向が逆になります。このルールは再帰的に適用されます。


サブタイプは適切です ここで、不変式を列挙できます。

コンパイラによって強制されるように不変条件をモデル化する方法については、多くの研究が進行中です。

タイプステート (3 ページを参照) 型に直交する状態不変条件を宣言し、強制します。あるいは、不変条件は次のように強制することもできます。 アサーションを型に変換する. 。たとえば、ファイルを閉じる前にファイルが開いていることを確認するには、File.open() は、File では使用できない close() メソッドを含む OpenFile 型を返すことができます。あ 三目並べ API これは、コンパイル時に不変条件を適用するために型付けを採用する別の例です。型システムはチューリング完全である場合もあります。 スカラ座. 。依存型型言語と定理証明者は、高次型付けのモデルを形式化します。

セマンティクスが必要なため、 拡張より抽象, 、私は型付けを採用して不変条件をモデル化することを期待しています。統一された高次の表示意味論は Typestate よりも優れています。「拡張」とは、調整されていないモジュール開発の無制限で並べ替えられた構成を意味します。なぜなら、相互に依存する 2 つのモデル (例:タイプと Typestate) を使用して共有セマンティクスを表現しますが、拡張可能な構成のために相互に統合することはできません。例えば、 式の問題-like 拡張機能は、サブタイプ化、関数のオーバーロード、およびパラメトリック型付けのドメインで統合されました。

私の理論上の立場は、 存在するための知識 (セクション「集中化は盲目的で不適切」を参照)、 一度もない これは、チューリング完全コンピュータ言語で考えられるすべての不変条件を 100% カバーすることを強制できる一般的なモデルです。知識が存在するためには、予期せぬ可能性がたくさん存在します。無秩序とエントロピーは常に増加しているはずです。これがエントロピー力です。潜在的な拡張のすべての可能な計算を証明することは、すべての可能な拡張を先験的に計算することです。

これが、停止定理が存在する理由です。チューリング完全プログラミング言語で考えられるすべてのプログラムが終了するかどうかは判断できません。特定のプログラムが終了することは証明できます (すべての可能性が定義され、計算されたプログラム)。しかし、そのプログラムの拡張の可能性がチューリング完全でない限り、そのプログラムのすべての可能な拡張が終了することを証明することは不可能です。依存型付け経由)。チューリング完全性の基本的な要件は次のとおりであるため、 無制限の再帰, 、ゲーデルの不完全性定理とラッセルのパラドックスが拡張にどのように適用されるかを直感的に理解できます。

これらの定理の解釈は、エントロピー力の一般化された概念的な理解に組み込まれます。

  • ゲーデルの不完全性定理:すべての算術的真理が証明できる形式理論はすべて矛盾しています。
  • ラッセルのパラドックス:セットを含むことができるセットのすべてのメンバーシップ ルールは、各メンバーの特定のタイプを列挙するか、それ自体を含みます。したがって、セットは拡張できないか、無制限の再帰になります。たとえば、ティーポット以外のすべてのセットには、ティーポット自体が含まれ、ティーポット自体が含まれ、ティーポット自体が含まれ、ティーポット自体が含まれます。したがって、ルールに特定のタイプ(つまり、セットが含まれている可能性があり、列挙されていない場合)が含まれていない場合、そのルールは一貫性がありません。指定されていないすべてのタイプを許可します)が、無制限の拡張は許可されません。これは、それ自体のメンバーではないセットのセットです。この一貫性がなく、すべての可能な拡張にわたって完全に列挙できないことは、ゲーデルの不完全性定理です。
  • リスコフ置換原則:一般に、ある集合が別の集合の部分集合であるかどうかは決定不可能な問題です。相続財産は一般的には決定できません。
  • リンスキーの言及:何かが記述されたり認識されたりするとき、それがどのような計算であるかは決定できません。認識(現実)には絶対的な基準点がありません。
  • コースの定理:外部の基準点がないため、無限の外部の可能性に対する障壁は機能しません。
  • 熱力学の第二法則:宇宙全体(閉じた系、つまりすべて)最大の無秩序への傾向、つまり最大限の独立した可能性。

LSP はクラスのコントラクトに関するルールです。基本クラスが規約を満たす場合、LSP によって派生クラスもその規約を満たす必要があります。

擬似Pythonで

class Base:
   def Foo(self, arg): 
       # *... do stuff*

class Derived(Base):
   def Foo(self, arg):
       # *... do stuff*

Derived オブジェクトで Foo を呼び出すたびに、arg が同じである限り、Base オブジェクトで Foo を呼び出した場合とまったく同じ結果が得られる場合、LSP は満たされます。

基本クラスへのポインターまたは参照を使用する関数は、意識せずに派生クラスのオブジェクトを使用できる必要があります。

最初に LSP について読んだとき、これは非常に厳密な意味での意味であり、本質的にはインターフェイスの実装とタイプセーフ キャストと同等の意味だと思いました。これは、LSP が言語自体によって保証されるかどうかのどちらかであることを意味します。たとえば、この厳密な意味では、コンパイラに関する限り、ThreeDBoard は確かに Board の代替可能です。

しかし、この概念について詳しく読んだところ、LSP は一般にそれよりも広く解釈されていることがわかりました。

つまり、クライアント コードにとって、ポインターの背後にあるオブジェクトがポインター型ではなく派生型であることを「認識」することの意味は、型安全性に限定されません。LSP への準拠は、オブジェクトの実際の動作を調査することによってテストすることもできます。つまり、オブジェクトの状態とメソッド引数がメソッド呼び出しの結果に及ぼす影響、またはオブジェクトからスローされる例外の種類を調査します。

もう一度例に戻りますと、 理論的には Board メソッドは ThreeDBoard 上で問題なく動作するように作成できます。しかし実際には、ThreeDBoard が追加しようとしている機能を妨げることなく、クライアントが適切に処理できない動作の違いを防ぐことは非常に困難です。

この知識を活用すれば、LSP の準拠性を評価することは、既存の機能を拡張するメカニズムとして、継承ではなく合成の方が適切であるかどうかを判断するための優れたツールとなります。

Liskovに違反しているかどうかを判断するためのチェックリストがあります。

  • 次の項目のいずれかに違反した場合 -> リスコフに違反したことになります。
  • 何も違反していない場合 -> 何も結論付けることはできません。

チェックリスト:

  • 派生クラスでは新しい例外をスローしてはなりません:基本クラスが ArgumentNullException をスローした場合、サブクラスは ArgumentNullException 型の例外、または ArgumentNullException から派生した例外のみをスローできます。IndexOutOfRangeException のスローは Liskov の違反です。
  • 前提条件は強化できない:基本クラスがメンバー int で動作すると仮定します。これで、サブタイプでは int が正である必要があります。これは前提条件が強化されており、負の整数を使用して以前は完全に正常に動作していたコードが壊れるようになりました。
  • 事後条件を弱めることはできない:基本クラスでは、メソッドが返される前にデータベースへのすべての接続を閉じる必要があるとします。サブクラスでそのメソッドをオーバーライドし、さらに再利用できるように接続を開いたままにしました。そのメソッドの事後条件を弱めました。
  • 不変条件は保存する必要がある:満たすのが最も難しく、苦痛を伴う制約。不変条件は基底クラスにしばらく隠されており、それを明らかにする唯一の方法は、基底クラスのコードを読み取ることです。基本的に、メソッドをオーバーライドするときは、変更できないものは、オーバーライドされたメソッドの実行後も変更されないままでなければならないことを確認する必要があります。私が考える最善の方法は、この不変制約を基本クラスに強制することですが、それは簡単ではありません。
  • 履歴制約:メソッドをオーバーライドする場合、基本クラス内の変更不可能なプロパティを変更することはできません。これらのコードを見ると、Name は変更不可 (プライベート セット) に定義されていますが、SubType では (リフレクションを通じて) 変更できる新しいメソッドが導入されていることがわかります。

    public class SuperType
    {
        public string Name { get; private set; }
        public SuperType(string name, int age)
        {
            Name = name;
            Age = age;
        }
    }
    public class SubType : SuperType
    {
        public void ChangeName(string newName)
        {
            var propertyType = base.GetType().GetProperty("Name").SetValue(this, newName);
        }
    }
    

他に 2 つのアイテムがあります: メソッド引数の反変性 そして 戻り値の型の共分散. 。しかし、C# ではそれは不可能なので (私は C# 開発者です)、私はそれらを気にしません。

参照:

重要な例としては、 使用 LSP が入っています ソフトウェアテスト.

B の LSP 準拠のサブクラスであるクラス A がある場合、B のテスト スイートを再利用して A をテストできます。

サブクラス A を完全にテストするには、おそらくさらにいくつかのテスト ケースを追加する必要がありますが、少なくともスーパークラス B のすべてのテスト ケースを再利用できます。

これを実現する方法は、マクレガーが「テスト用の並列階層」と呼ぶものを構築することです。私の ATest クラスはから継承します BTest. 。テスト ケースがタイプ B ではなくタイプ A のオブジェクトで動作することを確認するには、何らかの形式のインジェクションが必要になります (単純なテンプレート メソッド パターンで十分です)。

すべてのサブクラス実装に対してスーパーテスト スイートを再利用することは、実際には、これらのサブクラス実装が LSP 準拠であることをテストする方法であることに注意してください。したがって、次のように主張することもできます。 すべき 任意のサブクラスのコンテキストでスーパークラスのテスト スイートを実行します。

Stackoverflow の質問に対する回答も参照してください。インターフェイスの実装をテストするために、一連の再利用可能なテストを実装できますか?"

LSP が技術的にどのようなものかを皆さんはある程度説明したと思います。基本的には、サブタイプの詳細を抽象化し、スーパータイプを安全に使用できるようにしたいと考えています。

したがって、リスコフには 3 つの基本的なルールがあります。

  1. 署名ルール:構文的には、サブタイプ内のスーパータイプのすべての操作の有効な実装が必要です。コンパイラがチェックできるもの。スローする例外を少なくし、少なくともスーパータイプ メソッドと同じくらいアクセスしやすくすることに関して、小さなルールがあります。

  2. メソッドのルール:これらの操作の実装は意味的に適切です。

    • 弱い前提条件:サブタイプ関数は、それ以上ではないにしても、少なくともスーパータイプが入力として受け取ったものを受け取る必要があります。
    • より強力な事後条件:これらは、スーパータイプ メソッドが生成した出力のサブセットを生成する必要があります。
  3. プロパティ ルール:これは個々の関数呼び出しを超えたものです。

    • 不変条件:常に真実であるものは真実であり続けなければなりません。例えば。セットのサイズが負になることはありません。
    • 進化的特性:通常は、不変性またはオブジェクトが取り得る状態の種類に関係します。あるいは、オブジェクトは拡大するだけで縮小しないため、サブタイプ メソッドがそれを行うべきではない可能性があります。

これらのプロパティはすべて保持する必要があり、追加のサブタイプ機能がスーパータイプのプロパティに違反すべきではありません。

これら 3 つのことに注意すれば、基礎となるものから抽象化され、疎結合のコードを作成できます。

ソース:Java でのプログラム開発 - Barbara Liskov

長さ 簡単に言うと、長方形は長方形、正方形は正方形のままにしておきます。実際の例では、親クラスを拡張する場合、正確な親 API を保持するか、それを拡張する必要があります。

あなたが持っているとしましょう ベース アイテムリポジトリ。

class ItemsRepository
{
    /**
    * @return int Returns number of deleted rows
    */
    public function delete()
    {
        // perform a delete query
        $numberOfDeletedRows = 10;

        return $numberOfDeletedRows;
    }
}

そしてそれを拡張するサブクラス:

class BadlyExtendedItemsRepository extends ItemsRepository
{
    /**
     * @return void Was suppose to return an INT like parent, but did not, breaks LSP
     */
    public function delete()
    {
        // perform a delete query
        $numberOfDeletedRows = 10;

        // we broke the behaviour of the parent class
        return;
    }
}

それなら、 クライアント Base ItemsRepository API を使用し、それに依存します。

/**
 * Class ItemsService is a client for public ItemsRepository "API" (the public delete method).
 *
 * Technically, I am able to pass into a constructor a sub-class of the ItemsRepository
 * but if the sub-class won't abide the base class API, the client will get broken.
 */
class ItemsService
{
    /**
     * @var ItemsRepository
     */
    private $itemsRepository;

    /**
     * @param ItemsRepository $itemsRepository
     */
    public function __construct(ItemsRepository $itemsRepository)
    {
        $this->itemsRepository = $itemsRepository;
    }

    /**
     * !!! Notice how this is suppose to return an int. My clients expect it based on the
     * ItemsRepository API in the constructor !!!
     *
     * @return int
     */
    public function delete()
    {
        return $this->itemsRepository->delete();
    }
} 

LSP 壊れたとき 置き換える のクラス サブクラスが API のコントラクトを破る.

class ItemsController
{
    /**
     * Valid delete action when using the base class.
     */
    public function validDeleteAction()
    {
        $itemsService = new ItemsService(new ItemsRepository());
        $numberOfDeletedItems = $itemsService->delete();

        // $numberOfDeletedItems is an INT :)
    }

    /**
     * Invalid delete action when using a subclass.
     */
    public function brokenDeleteAction()
    {
        $itemsService = new ItemsService(new BadlyExtendedItemsRepository());
        $numberOfDeletedItems = $itemsService->delete();

        // $numberOfDeletedItems is a NULL :(
    }
}

保守可能なソフトウェアの作成については、私のコースで詳しく学ぶことができます。 https://www.udemy.com/enterprise-php/

この LSP の定式化は強すぎます。

型 S のオブジェクト o1 ごとに型 T のオブジェクト o2 があり、T に関して定義されたすべてのプログラム P について、o1 を o2 に置き換えても P の動作が変わらない場合、S は T のサブタイプになります。

これは基本的に、S が T とまったく同じものを完全にカプセル化した別の実装であることを意味します。そして、私は大胆になって、パフォーマンスが P の行動の一部であると判断することもできます...

したがって、基本的に、遅延バインディングの使用は LSP に違反します。オブジェクト指向の要点は、ある種類のオブジェクトを別の種類のオブジェクトに置き換えたときに異なる動作を実現することです。

引用された処方 ウィキペディアによる プロパティはコンテキストに依存し、必ずしもプログラムの動作全体が含まれるわけではないため、このプロパティの方が優れています。

いくつかの補足:
なぜ誰も、派生クラスが従わなければならない基底クラスの Invariant 、事前条件、事後条件について書かなかったのだろうか。派生クラス D が基本クラス B によって完全に代替可能であるためには、クラス D は特定の条件に従う必要があります。

  • 基本クラスの内部バリアントは派生クラスによって保持される必要がある
  • 基本クラスの前提条件を派生クラスによって強化してはなりません
  • 基本クラスの事後条件は、派生クラスによって弱められてはなりません。

したがって、派生クラスは、基本クラスによって課される上記の 3 つの条件を認識する必要があります。したがって、サブタイプのルールは事前に決定されています。つまり、「IS A」関係は、サブタイプが特定のルールに従う場合にのみ従うことになります。これらのルールは、不変条件、事前条件、事後条件の形式で、正式な '設計契約書'.

これに関するさらなる議論は私のブログでご覧いただけます: リスコフ置換原理

非常に簡単な文で次のように言えます。

子クラスは、その基本クラスの特性に違反してはなりません。それができるはずです。サブタイピングと同じだと言えます。

すべての回答に長方形と正方形があり、LSP に違反する方法がわかります。

実際の例を使用して LSP をどのように適合できるかを示したいと思います。

<?php

interface Database 
{
    public function selectQuery(string $sql): array;
}

class SQLiteDatabase implements Database
{
    public function selectQuery(string $sql): array
    {
        // sqlite specific code

        return $result;
    }
}

class MySQLDatabase implements Database
{
    public function selectQuery(string $sql): array
    {
        // mysql specific code

        return $result; 
    }
}

使用する実装に関係なく動作が変わらないため、この設計は LSP に準拠しています。

そして、はい、次のような簡単な変更を 1 つ行うだけで、この構成で LSP に違反する可能性があります。

<?php

interface Database 
{
    public function selectQuery(string $sql): array;
}

class SQLiteDatabase implements Database
{
    public function selectQuery(string $sql): array
    {
        // sqlite specific code

        return $result;
    }
}

class MySQLDatabase implements Database
{
    public function selectQuery(string $sql): array
    {
        // mysql specific code

        return ['result' => $result]; // This violates LSP !
    }
}

現在、サブタイプは同じ結果を生成しないため、同じ方法で使用することはできません。

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

常にプログラムモジュールを設計し、クラスの階層を作成します。次に、いくつかのクラスを拡張して、いくつかの派生クラスを作成します。

古いクラスの機能を置き換えることなく、新しい派生クラスが拡張されることを確認する必要があります。それ以外の場合、新しいクラスは、既存のプログラムモジュールで使用される場合、望ましくない効果を生成できます。

Liskovの代替原則は、プログラムモジュールが基本クラスを使用している場合、ベースクラスへの参照をプログラムモジュールの機能に影響を与えることなく、派生クラスに置き換えることができると述べています。

例:

以下は、リスコフの置換原則に違反する典型的な例です。この例では、2 つのクラスが使用されています。長方形と正方形。Rectangle オブジェクトがアプリケーションのどこかで使用されていると仮定します。アプリケーションを拡張し、Square クラスを追加します。square クラスは、いくつかの条件に基づいてファクトリ パターンによって返されますが、どのタイプのオブジェクトが返されるのか正確にはわかりません。しかし、それが Rectangle であることはわかっています。長方形オブジェクトを取得し、幅を 5、高さを 10 に設定して、面積を取得します。幅 5、高さ 10 の長方形の場合、面積は 50 になります。代わりに、結果は 100 になります。

    // Violation of Likov's Substitution Principle
class Rectangle {
    protected int m_width;
    protected int m_height;

    public void setWidth(int width) {
        m_width = width;
    }

    public void setHeight(int height) {
        m_height = height;
    }

    public int getWidth() {
        return m_width;
    }

    public int getHeight() {
        return m_height;
    }

    public int getArea() {
        return m_width * m_height;
    }
}

class Square extends Rectangle {
    public void setWidth(int width) {
        m_width = width;
        m_height = width;
    }

    public void setHeight(int height) {
        m_width = height;
        m_height = height;
    }

}

class LspTest {
    private static Rectangle getNewRectangle() {
        // it can be an object returned by some factory ...
        return new Square();
    }

    public static void main(String args[]) {
        Rectangle r = LspTest.getNewRectangle();

        r.setWidth(5);
        r.setHeight(10);
        // user knows that r it's a rectangle.
        // It assumes that he's able to set the width and height as for the base
        // class

        System.out.println(r.getArea());
        // now he's surprised to see that the area is 100 instead of 50.
    }
}

結論:

この原則は、オープンな密接な原則の単なる拡張であり、新しい派生クラスが動作を変更せずに基本クラスを拡張していることを確認する必要があることを意味します。

以下も参照してください。 オープンクローズの原則

構造を改善するための同様の概念をいくつか示します。 設定よりも規約

Board の配列に関して ThreeDBoard を実装することはそれほど便利でしょうか?

おそらく、さまざまなプレーンの ThreeDBoard のスライスをボードとして扱いたいと思うかもしれません。その場合、Board のインターフェイス (または抽象クラス) を抽象化して、複数の実装を可能にすることができます。

外部インターフェイスに関しては、TwoDBoard と ThreeDBoard の両方の Board インターフェイスを除外することをお勧めします (ただし、上記の方法はどれも適合しません)。

正方形とは、幅と高さが等しい長方形です。正方形が幅と高さの 2 つの異なるサイズを設定する場合、正方形の不変条件に違反します。これは副作用を導入することで回避されます。ただし、長方形に setSize(height, width) があり、前提条件が 0 < 高さ、0 < 幅である場合。派生サブタイプ メソッドには高さ == 幅が必要です。より強力な前提条件 (そしてそれは lsp に違反します)。これは、正方形は長方形ですが、前提条件が強化されているため、有効なサブタイプではないことを示しています。この回避策 (一般に悪いこと) は副作用を引き起こし、これにより事後条件が弱められます (これは lsp に違反します)。ベースの setWidth には事後条件 0 < width があります。派生では高さ == 幅で弱めます。

したがって、サイズ変更可能な正方形はサイズ変更可能な長方形ではありません。

コードで長方形を使用するとします。

r = new Rectangle();
// ...
r.setDimensions(1,2);
r.fill(colors.red());
canvas.draw(r);

幾何学の授業では、正方形は幅が高さと同じ長さであるため、特別なタイプの長方形であることを学びました。を作りましょう Square クラスもこの情報に基づいています:

class Square extends Rectangle {
    setDimensions(width, height){
        assert(width == height);
        super.setDimensions(width, height);
    }
} 

置き換えると、 RectangleSquare 最初のコードでは、次のように壊れます。

r = new Square();
// ...
r.setDimensions(1,2); // assertion width == height failed
r.fill(colors.red());
canvas.draw(r);

これは、 Square にはなかった新しい前提条件があります Rectangle クラス: width == height. 。LSPによると、 Rectangle インスタンスは次のものと置き換え可能である必要があります Rectangle サブクラスインスタンス。これは、これらのインスタンスが次の型チェックに合格するためです。 Rectangle インスタンスが存在するため、コード内で予期しないエラーが発生する可能性があります。

これは、 「サブタイプでは前提条件を強化できません」 の一部 ウィキ記事. 。要約すると、LSP に違反すると、ある時点でコードでエラーが発生する可能性があります。

Java で説明してみましょう。

class TrasportationDevice
{
   String name;
   String getName() { ... }
   void setName(String n) { ... }

   double speed;
   double getSpeed() { ... }
   void setSpeed(double d) { ... }

   Engine engine;
   Engine getEngine() { ... }
   void setEngine(Engine e) { ... }

   void startEngine() { ... }
}

class Car extends TransportationDevice
{
   @Override
   void startEngine() { ... }
}

ここは問題ないですよね?車は間違いなく交通手段であり、ここでは、車がそのスーパークラスの startEngine() メソッドをオーバーライドしていることがわかります。

別の交通手段を追加してみましょう。

class Bicycle extends TransportationDevice
{
   @Override
   void startEngine() /*problem!*/
}

今はすべてが計画通りに進んでいません!はい、自転車は移動手段ですが、エンジンがないため、startEngine() メソッドを実装できません。

これらは、リスコフの代替原則の違反が導く問題の種類であり、ほとんどの場合、何もしない、または実装できない方法によってほとんど認識できます。

これらの問題の解決策は正しい継承階層であり、私たちの場合、エンジンの有無にかかわらず輸送装置のクラスを区別することで問題を解決します。自転車は移動手段ですが、エンジンがありません。この例では、輸送装置の定義が間違っています。エンジンがあってはいけません。

TransportationDevice クラスを次のようにリファクタリングできます。

class TrasportationDevice
{
   String name;
   String getName() { ... }
   void setName(String n) { ... }

   double speed;
   double getSpeed() { ... }
   void setSpeed(double d) { ... }
}

TransportationDevice を非電動デバイス用に拡張できるようになりました。

class DevicesWithoutEngines extends TransportationDevice
{  
   void startMoving() { ... }
}

また、電動デバイス用に TransportationDevice を拡張します。ここでは、Engine オブジェクトを追加する方が適切です。

class DevicesWithEngines extends TransportationDevice
{  
   Engine engine;
   Engine getEngine() { ... }
   void setEngine(Engine e) { ... }

   void startEngine() { ... }
}

したがって、Liskov 置換原則を遵守しながら、Car クラスはより特殊化されます。

class Car extends DevicesWithEngines
{
   @Override
   void startEngine() { ... }
}

そして、私たちの自転車クラスもリスコフ置換原則に準拠しています。

class Bicycle extends DevicesWithoutEngines
{
   @Override
   void startMoving() { ... }
}

ぜひ次の記事を読んでください。 リスコフ置換原則 (LSP) への違反.

そこには、リスコフ置換原則とは何かについての説明、すでに違反しているかどうかを推測するのに役立つ一般的なヒント、クラス階層をより安全にするのに役立つアプローチの例が記載されています。

私がこれまでに見つけた LSP の最も明確な説明は、「リスコフ置換原則によれば、派生クラスのオブジェクトは、システムにエラーを引き起こしたり、基底クラスの動作を変更したりすることなく、基底クラスのオブジェクトを置き換えることができるはずです」 " から ここ. 。この記事では、LSP に違反し、それを修正するコード例を示します。

LISKOV 置換原則 (Mark Seemann の著書より) では、クライアントや実装を壊すことなく、インターフェイスのある実装を別の実装に置き換えることができる必要があると述べています。将来発生する要件に対処できるようにするのは、この原則です。今日はそれらが起こるとは予想できません。

コンピュータを壁から抜いた場合 (実装)、壁のコンセント (インターフェイス) もコンピュータ (クライアント) も故障しません (実際、ラップトップ コンピュータであれば、一定期間バッテリーで動作することもできます)。 。ただし、ソフトウェアの場合、クライアントはサービスが利用できることを期待することがよくあります。サービスが削除された場合は、NullReferenceException が発生します。このタイプの状況に対処するために、「何もない」ことを行うインターフェイスの実装を作成できます。これは、nullオブジェクト[4]と呼ばれるデザインパターンであり、壁からコンピューターのプラグを抜くことに大まかに対応しています。疎結合を使用しているため、問題を引き起こすことなく実際の実装を何も行わないものに置き換えることができます。

リコフの置換原理では次のように述べられています。 プログラム モジュールが Base クラスを使用している場合、プログラム モジュールの機能に影響を与えることなく、Base クラスへの参照を派生クラスに置き換えることができます。

意図 - 派生型は、基本型を完全に置き換えることができる必要があります。

例 - Java の共変戻り値の型。

LSP は、「オブジェクトはサブタイプによって置き換え可能であるべきである」と述べています。一方、この原則が指摘するのは、

子クラスは親クラスの型定義を決して壊してはいけません。

次の例は、LSP をより深く理解するのに役立ちます。

LSP なし:

public interface CustomerLayout{

    public void render();
}


public FreeCustomer implements CustomerLayout {
     ...
    @Override
    public void render(){
        //code
    }
}


public PremiumCustomer implements CustomerLayout{
    ...
    @Override
    public void render(){
        if(!hasSeenAd)
            return; //it isn`t rendered in this case
        //code
    }
}

public void renderView(CustomerLayout layout){
    layout.render();
}

LSPによる修正:

public interface CustomerLayout{
    public void render();
}


public FreeCustomer implements CustomerLayout {
     ...
    @Override
    public void render(){
        //code
    }
}


public PremiumCustomer implements CustomerLayout{
    ...
    @Override
    public void render(){
        if(!hasSeenAd)
            showAd();//it has a specific behavior based on its requirement
        //code
    }
}

public void renderView(CustomerLayout layout){
    layout.render();
}

インターフェースを考えてみましょう:

interface Planet{
}

これはクラスによって実装されます。

class Earth implements Planet {
    public $radius;
    public function construct($radius) {
        $this->radius = $radius;
    }
}

Earth を次のように使用します。

$planet = new Earth(6371);
$calc = new SurfaceAreaCalculator($planet);
$calc->output();

次に、Earth を拡張するもう 1 つのクラスを考えてみましょう。

class LiveablePlanet extends Earth{
   public function color(){
   }
}

LSP によると、Earth の代わりに LiveablePlanet を使用できるはずであり、システムが壊れることはありません。のように:

$planet = new LiveablePlanet(6371);  // Earlier we were using Earth here
$calc = new SurfaceAreaCalculator($planet);
$calc->output();

以下から抜粋した例 ここ

以下はからの抜粋です この郵便受け これは物事をうまく明確にします:

[..] いくつかの原則を理解するには、それがいつ違反されたかを認識することが重要です。これがこれからやることです。

この原則に違反するとはどういう意味ですか?これは、オブジェクトがインターフェイスで表現された抽象化によって課せられた契約を履行していないことを意味します。言い換えれば、それは自分の抽象化が間違っていると認識したことを意味します。

次の例を考えてみましょう。

interface Account
{
    /**
     * Withdraw $money amount from this account.
     *
     * @param Money $money
     * @return mixed
     */
    public function withdraw(Money $money);
}
class DefaultAccount implements Account
{
    private $balance;
    public function withdraw(Money $money)
    {
        if (!$this->enoughMoney($money)) {
            return;
        }
        $this->balance->subtract($money);
    }
}

これは LSP の違反ですか?はい。これは、アカウントの契約でアカウントが引き落とされると規定されているためですが、常にそうなるとは限りません。それで、それを修正するにはどうすればよいでしょうか?契約を変更するだけです。

interface Account
{
    /**
     * Withdraw $money amount from this account if its balance is enough.
     * Otherwise do nothing.
     *
     * @param Money $money
     * @return mixed
     */
    public function withdraw(Money $money);
}

これで契約は完了です。

この微妙な違反により、クライアントは使用されている具体的なオブジェクトの違いを見分ける能力が求められることがよくあります。たとえば、最初のアカウントの契約は次のようになります。

class Client
{
    public function go(Account $account, Money $money)
    {
        if ($account instanceof DefaultAccount && !$account->hasEnoughMoney($money)) {
            return;
        }
        $account->withdraw($money);
    }
}

そして、これは自動的にオープンクローズ原則(つまり、出金要件)に違反します。なぜなら、契約に違反したオブジェクトに十分な資金がなかったら何が起こるか決して分からないからです。おそらく何も返さないだけで、例外がスローされるでしょう。だから、それがあるかどうかを確認する必要があります hasEnoughMoney() -- これはインターフェースの一部ではありません。したがって、この強制的な具象クラス依存のチェックは OCP 違反です。]

この点は、LSP 違反に関してよく遭遇する誤解にも対処します。「親の行動が子供の中で変化した場合、LSPに違反する」と書かれています。ただし、子供が親の契約に違反していない限り、そうではありません。

ライセンス: CC-BY-SA帰属
所属していません StackOverflow
scroll top