リスコフ代替原則の例は何ですか?


908

Liskov Substitution Principle(LSP)はオブジェクト指向設計の基本原理であると聞いています。それは何ですか、そしてその使用のいくつかの例は何ですか?


LSPの遵守と違反のより多くの例ここでは
StuartLC

1
この質問には無限に多くの良い答えがあるので、広すぎます。
Raedwald

回答:


892

LSP(最近聞いたポッドキャストでボブおじさんから与えられた)を示す優れた例は、自然言語で正しく聞こえるものがコードでまったく機能しない場合があることです。

数学では、a SquareRectangleです。実際、これは長方形の特殊化です。「is a」はこれを継承でモデル化したい場合に使用します。ただし、コード内でSquareから派生させた場合Rectangle、は、Square期待する場所であればどこでも使用できるはずですRectangle。これは奇妙な振る舞いをします。

基本クラスにSetWidthSetHeightメソッドがあると想像してくださいRectangle。これは完全に論理的なようです。あなたの場合はRectangle、参照が指されSquare、その後、SetWidthおよびSetHeight1を設定すると、それに合わせて他の変更になるので意味がありません。この場合Square、リスコフ置換テストは失敗し、継承Rectangleを行うという抽象化は悪いものです。SquareRectangle

ここに画像の説明を入力してください

Y'allは、他の貴重なSOLID Principles Motivational Postersをチェックする必要があります。


19
@ m-sharp SetWidthやSetHeightの代わりに、GetWidthメソッドやGetHeightメソッドを使用するような不変のRectangleの場合はどうでしょうか。
パセリエ2012

139
物語の教訓:プロパティではなく動作に基づいてクラスをモデル化します。行動ではなくプロパティに基づいてデータをモデル化します。それがアヒルのように振る舞うなら、それは確かに鳥です。
Sklivvz

193
まあ、正方形は明らかに現実の世界では長方形の一種です。これをコードでモデル化できるかどうかは、仕様によって異なります。LSPが示すことは、サブタイプの動作は、基本型の仕様で定義されている基本型の動作と一致する必要があるということです。長方形の基本タイプの仕様で、高さと幅を個別に設定できることが示されている場合、LSPは、正方形を長方形のサブタイプにすることはできないと述べています。四角形の仕様で、四角形は不変であると示されている場合、正方形は四角形のサブタイプになる場合があります。すべては、基本型に指定された動作を維持するサブタイプに関するものです。
SteveT 2012

63
@Pacerierは不変であれば問題はありません。ここでの本当の問題は、長方形をモデル化するのではなく、「再形成可能な長方形」、つまり作成後に幅または高さを変更できる長方形であるということです(それでも同じオブジェクトと見なします)。この方法で四角形クラスを見ると、四角形は形を変えられず、なおかつ四角形であることができないため(一般的に)、四角形は「変形可能な四角形」ではないことは明らかです。数学的には、可変性は数学的な文脈では意味をなさないため、問題は発生しません。
asmeurer 2013年

14
原則について一つ質問があります。が次のようSquare.setWidth(int width)に実装されている場合、なぜ問題になるでしょうthis.width = width; this.height = width;か?この場合、幅が高さに等しいことが保証されます。
MC皇帝、

488

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

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

中心となるLSPは、インターフェイスとコントラクト、およびクラスを拡張するタイミングと、構成などの別の戦略を使用して目的を達成するタイミングを決定する方法についてです。

この点を説明するために私が見た最も効果的な方法は、Head First OOA&Dでした。彼らは、戦略ゲームのフレームワークを構築するプロジェクトの開発者であるシナリオを提示します。

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

クラス図

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

この本は、ゲームフレームワークが3Dゲームボードもサポートしてフライトを伴うゲームに対応する必要があると言うために、要件を変更し続けます。したがって、ThreeDBoard拡張するクラスが導入されますBoard

一見すると、これは良い決断のようです。とプロパティのBoard両方を提供し、Z軸を提供します。HeightWidthThreeDBoard

内訳は、から継承された他のすべてのメンバーを見るとわかりますBoard。方法はAddUnitGetTileGetUnitsというように、すべてのXおよびYパラメータの両方を取るBoardクラスが、ThreeDBoard井戸としてZパラメータを必要とします。

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

多分私たちは別のアプローチを見つける必要があります。代わりに拡張でBoardThreeDBoardで構成されなければならないBoardオブジェクト。BoardZ軸の単位ごとに1 つのオブジェクト。

これにより、カプセル化や再利用などの適切なオブジェクト指向の原則を使用でき、LSPに違反しません。


10
同様の簡単な例については、WikipediaのCircle-Ellipse Problemも参照してください。
ブライアン、

@NotMySelfからの引用:「この例は、ThreeDBoardのコンテキストではボードからの継承が意味をなさず、Z軸ではすべてのメソッドシグネチャが無意味であることを単に示すためのものだと思います。」
Contango 2013年

1
それでは、Childクラスに別のメソッドを追加しても、ChildクラスでParentのすべての機能がまだ意味をなす場合、LSPが機能しなくなるのでしょうか。一方では、Childを使用するためのインターフェースを少し変更したので、他方で、ChildをParentにキャストすると、Parentを期待するコードが正常に機能します。
Nickolay Kondratyev 2013

5
これは反リスコフの例です。Liskovは、四角形から四角形を派生させます。less-parameters-classからmore-parameters-class。そして、あなたはそれが悪いことをうまく示しています。答えとしてマークし、リスコフの質問に対する反リスコフの回答の200倍に投票したのは、本当に良いジョークです。リスコフの原則は本当に誤りですか?
Gangnus

3
継承が間違った方法で機能するのを見てきました。ここに例があります。基本クラスは3DBoardおよび派生クラスBoardでなければなりません。ボードのZ軸は引き続きMax(Z)= Min(Z)= 1
Paulustrious

169

代替可能性は、オブジェクト指向プログラミングの原則であり、コンピュータプログラムでは、SがTのサブタイプである場合、タイプTのオブジェクトはタイプSのオブジェクトに置き換えられる可能性があると述べています。

Javaで簡単な例を見てみましょう:

悪い例

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

アヒルは鳥なので飛ぶことができますが、これはどうですか?

public class Ostrich extends Bird{}

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

良い手本

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

3
良い例ですが、クライアントが持っている場合はどうしますかBird bird。フライを使用するには、オブジェクトをFlyingBirdsにキャストする必要がありますが、これは適切ではありませんか?
ムーディー

17
いいえ。クライアントにがある場合、Bird birdそれは使用できませんfly()。それでおしまい。aを渡してDuckもこの事実は変わりません。クライアントがを持っている場合、FlyingBirds birdそれが渡されても、Duck常に同じように動作するはずです。
Steve Chamaillard

9
これは、インターフェースの分離の良い例にもなりませんか?
Saharsh、

優れた例ありがとうMan
Abdelhadi Abdo

6
インターフェース 'Flyable'を使用するのはどうですか(これ以上の名前は考えられません)。このようにして、この厳密な階層構造に固執することはありません。本当に必要な場合を除いて、
Thirdy

132

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

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

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

class Square : Rectangle { }

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

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

ただし、この不変条件の正しい実装に違反している必要があるSquareため、の有効な代用にはなりませんRectangle


35
したがって、「OO」を使用して実際にモデル化したいすべてのものをモデル化するのは困難です。
DrPizza 2009年

9
@DrPizza:もちろんです。ただし、2つのこと。まず、このような関係ができ、まだ不完全またはより複雑な迂回路を使用しながらも、OOPでモデル化すること(どちらスーツあなたの問題を選択)。第二に、これ以上の選択肢はありません。他のマッピング/モデリングには、同じまたは同様の問題があります。;-)
Konrad Rudolph

7
@NickW場合によっては(ただし、上記ではない)継承チェーンを単純に逆にすることができます。論理的には、2Dポイントは3Dポイントであり、3番目の次元は無視されます(または0-すべてのポイントが同じ平面上にある) 3Dスペース)。しかし、これはもちろん実際的ではありません。一般に、これは継承が実際には役に立たず、エンティティ間に自然な関係が存在しない場合の1つです。それらを別々にモデル化します(少なくとも、私はより良い方法を知りません)。
Konrad Rudolph

7
OOPは、データではなく動作をモデル化することを目的としています。LSPに違反する前でも、クラスはカプセル化に違反しています。
Sklivvz

2
@AustinWBryanうん この分野での作業が長くなるほど、インターフェイスと抽象基本クラスのみに継承を使用し、残りの部分には構成を使用する傾向があります。これは(タイピングに関して)少し手間がかかる場合がありますが、一連の問題を回避し、他の経験豊富なプログラマーから広く反響を受けています。
Konrad Rudolph

77

ロバート・マーティンは、リスコフ代替原則に関する優れた論文を持っています。原則に違反する可能性のある微妙な方法とそれほど微妙ではない方法について説明します。

論文のいくつかの関連部分(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たものとしてモデル化するのが論理的です。[...]

SquareSetWidthおよびSetHeight関数を継承します。Square正方形の幅と高さは同じであるため、これらの関数はにはまったく不適切です。これは、デザインに問題があることを示す重要な手がかりとなるはずです。ただし、問題を回避する方法があります。オーバーライドしてSetWidthSetHeight[...]

ただし、次の関数を検討してください。

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

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

[...]


14
かなり遅れましたが、私はこれがその論文で興味深い引用だと思いました: Now the rule for the preconditions and postconditions for derivatives, as stated by Meyer is: ...when redefining a routine [in a derivative], you may only replace its precondition by a weaker one, and its postcondition by a stronger one. 子クラスの前提条件が親クラスの前提条件よりも強い場合、前提条件に違反することなく親を子供に置き換えることはできません。したがって、LSP。
user2023861 2015

@ user2023861あなたは完全に正しいです。これに基づいて答えを書きます。
inf3rno 2017年

40

LSPは、いくつかのコードは、それが型のメソッドを呼び出していると考えて、必要な場合でありT、かつ無意識のタイプのメソッドを呼び出すことができるSS extends T(すなわちS継承導出から、またはスーパータイプのサブタイプですT)。

たとえば、タイプの入力パラメーターを持つ関数が、タイプTの引数値で呼び出される(呼び出される)場合に発生しSます。または、typeの識別子にはtype Tの値が割り当てられます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

不変フィールドを持つタイプでも不変です。たとえば、不変の Rectangleセッターは次元が個別に変更されることを期待しますが、不変の Squareセッターはこの期待に違反します。

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と同じか、スーパータイプでなければなりません。。TiT

共分散とは、分散が継承と同じ方向であることを意味します。つまり、Soサブタイプの各メソッドの出力の型は、スーパータイプの対応するメソッドの対応する出力の型Sと同じか、サブタイプでなければなりません。ToT

これは、呼び出し元が型を持っているTと考え、のメソッドを呼び出していると考えた場合、型のT引数を提供しTi、出力を型に割り当てToます。実際にの対応するメソッドを呼び出している場合S、各Ti入力引数はSi入力パラメーターにSo割り当てられ、出力は型に割り当てられToます。したがって、Siに対して反変ではない場合Ti、サブタイプXi(のサブタイプではない)をSiに割り当てることができTiます。

さらに、型多型パラメーター(つまりジェネリック)に定義サイト差異アノテーションがある言語(ScalaやCeylonなど)の場合、型の各型パラメーターの差異アノテーションの同方向または反対方向Tは、反対または同じ方向でなければなりません。T型パラメーターの型を持つ(のすべてのメソッドの)すべての入力パラメーターまたは出力にそれぞれ。

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


サブタイピングは、不変条件を列挙できる場合に適しています。

不変条件をモデル化する方法に関する多くの進行中の研究があり、それらはコンパイラーによって強制されます。

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

拡張を超え抽象化するセマンティクスの必要性のため、不変式をモデル化するために型付けを使用すること、つまり統一された高次の表示セマンティクスがTypestateよりも優れていることを期待しています。「拡張」とは、調整されていないモジュール化された開発の無制限の順列構成を意味します。私には統一とそれによる自由度の正反対であると思われるため、拡張可能な合成のために互いに統一することができない共有されたセマンティクスを表現するための2つの相互に依存するモデル(たとえば、タイプとTypestate)を持つこと。たとえば、Expression Problemのような拡張機能は、サブタイピング、関数のオーバーロード、およびパラメトリックタイピングドメインに統合されました。

私の理論的立場は、知識が存在するため(「集中化は盲目的で不適切」のセクションを参照)、チューリング完全なコンピューター言語ですべての可能な不変式を100%カバーできる一般的なモデルは決してないということです。知識が存在するためには、予想外の可能性が多く存在します。つまり、無秩序とエントロピーは常に増加している必要があります。これがエントロピー力です。潜在的な拡張のすべての可能な計算を証明するには、すべての可能な拡張をアプリオリに計算します。

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

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

  • ゲーデルの不完全性定理:すべての算術的真理を証明できる形式理論には一貫性がありません。
  • ラッセルのパラドックス:セットを含むことができるセットのすべてのメンバーシップルールは、各メンバーの特定のタイプを列挙するか、またはそれ自体を含みます。したがって、セットは拡張できないか、制限のない再帰です。たとえば、ティーポットではないすべてのセットには、それ自体が含まれ、それ自体が含まれ、それ自体が含まれます。したがって、特定のタイプを列挙せず(つまり、すべての指定されていないタイプを許可する)、制限のない拡張を許可しない場合(セットを含む可能性がある)、ルールには一貫性がありません。これは、自分自身のメンバーではないセットのセットです。一貫性があり、すべての可能な拡張にわたって完全に列挙できないことは、ゲーデルの不完全性定理です。
  • Liskov Substition Principle:一般に、いずれかのセットが別のセットのサブセットであるかどうか、つまり継承は一般に決定不可能かどうかは、決定不能な問題です。
  • Linsky参照:何かの計算が何であるか、それが記述または認識されている場合、つまり、認識(現実)には絶対的な参照ポイントがありません。
  • コースの定理:外部参照ポイントがないため、制限のない外部の可能性に対するバリアは失敗します。
  • 熱力学の第2法則:宇宙全体(閉じたシステム、つまりすべて)は、最大の無秩序、つまり最大の独立した可能性に向かいます。

17
@シェリービー:あなたはあまりにも多くのものを混ぜました。物事はあなたがそれらを述べるほど混乱しない。あなたの理論的主張の多くは、「知識が存在するためには、予期しない可能性が非常に存在する...」などの薄弱な根拠に基づいており、「一般に、いずれかのセットが別のセットのサブセットであるかどうかは決定不能な問題です。継承は一般に決定不可能です。これらのポイントごとに個別のブログを開始できます。とにかく、あなたの主張と仮定は非常に疑わしいものです。気づいていないものを使ってはいけません!
aknon 2013

1
@aknon私はこれらの問題をより深く説明するブログ持っています。私の無限時空のTOEモデルは、無限の周波数です。再帰的帰納関数が無限の終了境界を持つ既知の開始値を持っているか、または共帰関数が未知の終了値と既知の開始境界を持っていることは私を混乱させません。再帰が導入されると、相対性が問題になります。これが、完全なチューリングが無制限の再帰に相当する理由です。
シェルビームーアIII

4
@ShelbyMooreIIIあなたはあまりにも多くの方向に進んでいます。これは答えではありません。
Soldalma 2016

1
@Soldalmaそれは答えです。回答セクションに表示されませんか?Yoursはコメントセクションにあるため、コメントです。
シェルビームーアIII

1
Scalaワールドとのミキシングのように!
Ehsan M. Kermani 2017年

24

すべての回答に長方形と正方形があり、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に準拠しています。これは、使用する実装に関係なく、動作が変化しないためです。

そして、はい、この構成でLSPに違反して、次のような1つの簡単な変更を行うことができます。

<?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 !
    }
}

サブタイプは、同じ結果を生成しないため、同じ方法で使用できなくなりました。


6
すべての DBエンジンでDatabase::selectQueryサポートされている SQLのサブセットのみをサポートするようにのセマンティクスを制限している限り、この例はLSPに違反しません。それはほとんど実用的ではありません...つまり、この例は、ここで使用されている他のほとんどの例よりもさらに簡単に理解できます。
Palec、2018

5
この答えは、残りの部分から把握するのが最も簡単であることがわかりました。
マルコムサルバドール

23

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つの他の項目がありますメソッドの引数のContravariance戻り値の型の共分散は。しかし、C#(私はC#開発者です)では不可能なので、私はそれらについて気にしません。

参照:


私はC#開発者でもあり、.Net 4.0フレームワークを使用するVisual Studio 2010では、最後のステートメントは当てはまらないと言います。戻り値の型の共分散により、インターフェースで定義されたものよりも多くの戻り値の型が可能になります。例:例:IEnumerable <T>(Tは共変)IEnumerator <T>(Tは共変)IQueryable <T>(Tは共変)IGrouping <TKey、TElement>(TKeyとTElementは共変)IComparer <T>(T is contravariant)IEqualityComparer <T>(Tは反変)IComparable <T>(Tは反変)msdn.microsoft.com/en-us/library/dd233059
v

1
素晴らしく焦点を絞った回答(ただし、元の質問はルールではなく例についてのものでしたが)。
Mike

22

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を満たします。


9
しかし...常に同じ動作をする場合、派生クラスがあることの意味は何ですか?
レオニード

2
ポイントを逃しました。これは、観察された動作と同じです。たとえば、あるものをO(n)のパフォーマンスで機能的に同等の何かに置き換えますが、O(lg n)のパフォーマンスで置き換えることができます。または、MySQLで実装されたデータにアクセスするものを置き換え、それをインメモリデータベースに置き換えることもできます。
チャーリーマーティン

@Charlie Martin、実装ではなくインターフェースにコーディングする-私はそれを掘る。これはOOPに固有のものではありません。Clojureなどの関数型言語もそれを促進します。JavaまたはC#の観点からも、抽象クラスとクラス階層を使用するのではなく、インターフェースを使用することは、提供する例では自然だと思います。Pythonは強く型付けされておらず、少なくとも明示的にはインターフェイスがありません。私の難点は、SOLIDに固執することなくOOPを数年間行っていることです。私がそれに遭遇した今、それは限定的でほとんど自己矛盾しているようです。
Hamish Grubijan

さて、戻ってバーバラのオリジナルの紙をチェックする必要があります。reports-archive.adm.cs.cmu.edu/anon/1999/CMU-CS-99-156.psこれは、インターフェースに関しては実際には述べられておらず、論理的な関係であり、何らかの形で継承されたプログラミング言語。
チャーリーマーティン

1
@HamishGrubijan Pythonが強く型付けされていないと誰が言ったかはわかりませんが、彼らはあなたに嘘をついていました(そして、あなたが私を信じていない場合は、Pythonインタープリターを起動してを試してください2 + "2")。「強く型付けされた」と「静的に型付けされた」を混同しているのではないでしょうか。
asmeurer 2013年

21

ロング親クラスを拡張する際にストーリー短い、LETの休暇の長方形の長方形や正方形の正方形、実用的な例は、次のいずれかの正確な親APIを維持するか、それを拡張する必要があります。

基本の ItemsRepository があるとしましょう。

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/


20

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

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

LSPは一般にそれよりも広く解釈されることがわかりましたが、コンセプトについてさらに読んだ後。

つまり、ポインタの背後にあるオブジェクトがポインタ型ではなく派生型であることをクライアントコードが「認識する」とは、タイプセーフに限定されないということです。LSPへの準拠は、オブジェクトの実際の動作を調べることでテストすることもできます。つまり、オブジェクトの状態とメソッド引数がメソッド呼び出しの結果、またはオブジェクトからスローされた例外のタイプに与える影響を調べます。

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

この知識があれば、LSPアドヒアランスの評価は、コンポジションが継承ではなく既存の機能を拡張するためのより適切なメカニズムであるかどうかを判断するための優れたツールになります。


19

誰もがLSPが技術的に何であるかをカバーしたと思います:あなたは基本的にサブタイプの詳細から抽象化し、スーパータイプを安全に使用したいです。

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

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

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

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

    • 不変条件:常に真であることが真であり続けなければなりません。例えば。セットのサイズが負になることはありません。
    • 進化的プロパティ:通常、不変性またはオブジェクトの状態の種類と関係があります。または、オブジェクトが成長するだけで縮小しないため、サブタイプのメソッドがそれを行わないようにする必要があります。

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

これらの3つの点が処理されると、基礎となるものから抽象化され、疎結合コードを記述していることになります。

出典:Javaでのプログラム開発-Barbara Liskov


18

LSP の使用の重要な例は、ソフトウェアのテストです。

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

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

これを実現する方法は、McGregorが「テスト用の並列階層」と呼ぶものを構築ATestすることですBTest。私のクラスはから継承します。テストケースがタイプBではなくタイプAのオブジェクトで機能するようにするには、何らかの注入が必要です(単純なテンプレートメソッドパターンで実行できます)。

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

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


14

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() { ... }
}

9

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

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

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

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

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


2
ええと、その定式化はBarbara Liskov自身のものです。バーバラリスコフ、「データの抽象化と階層」、SIGPLANの通知、23、5(1988年5月)。それは「強すぎる方法」ではなく、「まったく正しい」であり、それがあなたが持っているとあなたが思う意味を持たない。丈夫ですが、適度な強さです。
DrPizza 2009年

次に、実際のサブタイプはごくわずかです:)
Damien Pollet

3
「動作は変更されていません」とは、サブタイプによってまったく同じ具体的な結果値が得られるという意味ではありません。これは、サブタイプの動作が基本タイプで期待される動作と一致することを意味します。例:基本タイプShapeはdraw()メソッドを持つことができ、このメソッドが形状をレンダリングする必要があることを規定します。Shapeの2つのサブタイプ(たとえば、SquareとCircle)は、両方ともdraw()メソッドを実装し、結果は異なって見えます。ただし、動作(形状のレンダリング)がShapeの指定された動作と一致する限り、正方形と円はLSPに従ってShapeのサブタイプになります。
SteveT

9

非常に単純な文で、次のように言うことができます。

子クラスは、基本クラスの特性に違反してはなりません。それに対応できなければなりません。サブタイピングと同じです。


9

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

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

古いクラスの機能を置き換えることなく、新しい派生クラスが拡張されることを確認する必要があります。そうしないと、新しいクラスが既存のプログラムモジュールで使用されたときに、新しいクラスが望ましくない影響を与える可能性があります。

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

例:

以下は、リスコフの代入原則に違反する典型的な例です。この例では、長方形と正方形の2つのクラスが使用されています。Rectangleオブジェクトがアプリケーションのどこかで使用されていると仮定しましょう。アプリケーションを拡張し、Squareクラスを追加します。正方形クラスは、いくつかの条件に基づいてファクトリパターンによって返されます。返されるオブジェクトのタイプは正確にはわかりません。しかし、それが四角形であることはわかっています。長方形オブジェクトを取得し、幅を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.
    }
}

結論:

この原則は、Open Close Principleの拡張に過ぎず、新しい派生クラスが基本クラスを、その動作を変更せずに拡張していることを確認する必要があることを意味します。

参照:オープンクローズの原則

より良い構造のためのいくつかの同様の概念:構成に関する規約


8

リスコフ代替原則

  • オーバーライドされたメソッドは空のままにしないでください
  • オーバーライドされたメソッドはエラーをスローするべきではありません
  • 派生クラスの動作が原因で、基本クラスまたはインターフェイスの動作を変更(リワーク)すべきではありません。

7

補遺:
派生クラスが従わなければならない基本クラスのInvariant、前提条件、事後条件について誰も書かなかったのはなぜでしょうか。派生クラスDが基本クラスBによって完全に支持されるためには、クラスDは特定の条件に従う必要があります。

  • 基本クラスの不変式は、派生クラスによって保持される必要があります
  • 基本クラスの前提条件は、派生クラスによって強化されてはなりません
  • 基本クラスの事後条件は、派生クラスによって弱められてはなりません。

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

これに関するさらなる議論は私のブログで利用できます:Liskov Substitution principal


6

簡単に言えば、LSPは、同じスーパークラスのオブジェクトは何も壊すことなく互いに交換できる必要があると述べています。

たとえば、CatDogクラスから派生したAnimalクラスがある場合、Animalクラスを使用するすべての関数は、正常に使用CatまたはDog動作できるはずです。


4

ボードの配列の観点からThreeDBoardを実装することは、それほど有用でしょうか?

おそらく、さまざまな面でThreeDBoardのスライスをボードとして扱うことをお勧めします。その場合、ボードのインターフェース(または抽象クラス)を抽象化して、複数の実装を可能にすることができます。

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


1
この例は、ThreeDBoardのコンテキストではボードからの継承が意味をなさず、すべてのメソッドシグネチャがZ軸では無意味であることを単に示すためのものだと思います。
NotMyself 2008

4

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

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


4

この原理は1987年にBarbara Liskovによって導入され、スーパークラスとそのサブタイプの動作に焦点を当てることにより、開閉原理を拡張しています。

違反の結果を考慮すると、その重要性が明らかになります。次のクラスを使用するアプリケーションを考えます。

public class Rectangle 
{ 
  private double width;

  private double height; 

  public double Width 
  { 
    get 
    { 
      return width; 
    } 
    set 
    { 
      width = value; 
    }
  } 

  public double Height 
  { 
    get 
    { 
      return height; 
    } 
    set 
    { 
      height = value; 
    } 
  } 
}

ある日、クライアントが長方形に加えて正方形を操作する機能を要求するとします。正方形は長方形なので、正方形クラスは長方形クラスから派生させる必要があります。

public class Square : Rectangle
{
} 

ただし、これを行うと、2つの問題が発生します。

正方形は、長方形から継承された高さと幅の両方の変数を必要としないため、数十万の正方形のオブジェクトを作成する必要がある場合、メモリにかなりの無駄が生じる可能性があります。四角形の幅と高さは同じであるため、四角形から継承された幅と高さの設定プロパティは正方形には不適切です。高さと幅の両方を同じ値に設定するために、次のように2つの新しいプロパティを作成できます。

public class Square : Rectangle
{
  public double SetWidth 
  { 
    set 
    { 
      base.Width = value; 
      base.Height = value; 
    } 
  } 

  public double SetHeight 
  { 
    set 
    { 
      base.Height = value; 
      base.Width = value; 
    } 
  } 
}

これで、誰かが正方形のオブジェクトの幅を設定すると、それに応じてその高さが変化し、逆も同様です。

Square s = new Square(); 
s.SetWidth(1); // Sets width and height to 1. 
s.SetHeight(2); // sets width and height to 2. 

先に進み、この他の機能を考えてみましょう:

public void A(Rectangle r) 
{ 
  r.SetWidth(32); // calls Rectangle.SetWidth 
} 

この関数に正方形のオブジェクトへの参照を渡すと、関数はその引数の導関数に対して機能しないため、LSPに違反します。プロパティwidthとheightは、長方形で仮想として宣言されていないため、ポリモーフィックではありません(高さが変更されないため、正方形のオブジェクトは破損します)。

ただし、セッタープロパティを仮想として宣言すると、OCPという別の違反が発生します。実際、派生クラスの正方形を作成すると、基本クラスの長方形が変更されます。


3

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


1
stackoverflowのコードの例を提供してください。
sebenalern、2016年

3

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

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ため、コードで予期しないエラーが発生するためです。

これは、wiki記事の「サブタイプでは前提条件を強化できない」部分の例でした。つまり、LSPに違反すると、ある時点でコードにエラーが発生する可能性があります。


3

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


2

LISKOV SUBSTITUTION PRINCIPLE(Mark Seemannの本より)は、クライアントまたは実装のいずれかを壊すことなく、インターフェイスのある実装を別の実装に置き換えることができるはずであると述べています。今日はそれらを予測します。

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


2

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

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

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


1

以下は、この記事からの抜粋で、わかりやすく説明しています。

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

この原則の違反とはどういう意味ですか?これは、オブジェクトが、インターフェースで表現された抽象化によって課された規約を満たさないことを意味します。つまり、抽象化を間違って識別したことになります。

次の例を考えてみます。

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に違反します」と言います。ただし、そうではありません—子が親の契約に違反しない限り。

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