私はリスコフの代替原則に頭を抱えていると思い続けていますが、そうではないことに気づきました。 StackExchangeから収集したのは次の場所です。
それらは私には互いに矛盾しているようです。これが悪い場合:
square.setWidth(40);
square.setHeight(50);
width = square.getWidth(); // Returns 50 instead of 40. Liskov hates that.
では、なぜこれが悪くないのですか?
nullRectangle.setWidth(40);
nullRectangle.setHeight(50);
width = nullRectangle.getWidth(); // Returns 0 or null. Isn't that worse?
ここで何が欠けていますか?
あなたが見つけたのは、Nullオブジェクトが良い解決策ではないかもしれない単純なEdgeケースです。
null object
のポイントは、事前にnull
をチェックした場合と同じ動作にすることです。
あなたの場合、このコードが予想される動作であれば:
Rectangle nullRectangle = null;
int width = 0;
if (nullRectangle != null)
{
nullRectangle.setWidth(40);
nullRectangle.setHeight(50);
width = nullRectangle.getWidth();
}
// width is 0 if nullRectangle is null
コードに記述されている10のnull object
は、論理的な代用です。
しかし、「ヌルの長方形」という考えは、モデリングの観点からはあまり意味がありません。 Nullオブジェクトは、実際には抽象化の実装としてのみ使用できます。メソッドを呼び出しても、呼び出し元が常に何かを期待しているわけではありません。抽象化のメソッドの呼び出し元が「操作なし」または「データが返されなかった」ことを有効な結果と見なした場合、null object
を使用できます。代わりにゼロ以外の値が戻り値として期待される場合、nullオブジェクトは本当にLSPに違反します。
長方形/正方形の問題は、誤ったドメイン分析の結果だと思います。
数学的長方形/正方形
数学から、正方形は四角形の特殊なケースであり、Square
をRectangle
のサブクラスに変換することは誰でも知っています。四角形の数学的概念を守っていれば問題ありません。正方形オブジェクトの幅または高さの変更は含まれません。不変の形状の場合、継承モデルはリスコフの原則に準拠しています。
ミュータブルプログラムオブジェクト
セッターを導入したらすぐに、 setHeight()
、数学的アナロジーはもはや成り立たないため、新しい分析を行う必要があります。
setHeight(50)
の意味?これをRectangle
クラスで見た場合、幅は同じままであるとみなされ、シェイプのアスペクト比が変更されます。これは、正方形に適用すると、非正方形の長方形になる操作です。したがって、Square.setHeight()
メソッドは、オブジェクトのクラスをRectangle
からSquare
に変更する必要があるため、Rectangle
- inspiredコントラクトに準拠できません(これは不可能です)私が知っているすべてのプログラミング言語で)。
したがって、Square
から継承する変更可能なRectangle
クラスを使用すると、Liskovに違反します。
ここでの悪の源は可変性です。 setHeight()
が元のインスタンスの状態を変更するのではなく、新しいインスタンスを返す場合、状況を解決できます。次に、Square
でsetHeight()
を呼び出すと、新しいディメンションでRectangle
を簡単に返すことができます。 Rectangle
から実装を継承することもできます。
古典的なSquare-Rectangle LSP問題の「トリック」は、コントラクトを満たさなければならないオブジェクトとして長方形を定義すると...
rectangle.setWidth(40);
rectangle.setHeight(50);
assert(rectangle.getWidth() == 40);
...それを正方形に拡張すると、その契約に違反します。これは、長方形のカスタム定義と従来の定義を混同しているためです。そのため、古典的な定義をカスタム定義に適合させようとしていますが、機能しません。本質的に、それはあなたがサブタイプの振る舞いと契約上の義務について考えることを意図するパーラートリックです。上記のRectangleオブジェクトのアサーションが、巧妙な言い換えのある長方形であっても失敗することを簡単に示すことができます。
rectangle.setTopEdgeLength(40);
rectangle.setBottomEdgeLength(50);
assert(rectangle.getTopEdgeLength() == 40);
とにかく、なぞなぞで十分です、見てみましょう...
古典的な定義
正しく言えば、正方形と長方形はどちらも四角形のタイプで、四角形は4つのエッジと4つの頂点を持つポリゴンです。古典的には、正方形is長方形ですが、長方形と正方形のWikipedia定義を見てみましょう。
そこに何か気づきましたか?長方形と正方形の間の唯一の共通点は、両方が四角形であり、両方に4つの直角があることです。側面の長さに関するルールはすべて言及されていません。これらは派生であり、契約の一部ではありません。これを簡単な論理ステートメントで示すことができます:
四角形と四角形の古典的な定義を維持したい場合andsetWidth()
メソッドとsetHeight()
メソッドを使用してコードでそれらを表す場合、コントラクトは次のようにする必要があります。
square.setWidth(40);
square.setHeight(50);
assert(square.isQuadrilateral());
assert(square.hasFourRightAngles());
幅が1行目で設定した幅であるかどうかは関係ありません。そのルールは親オブジェクトRectangleの契約上の義務の一部を構成しないためです。
要約すると、最初の問題は契約上の定義の1つです。 rectangle.setWidth(40); rectangle.setHeight(50); assert(rectangle.getWidth() == 40);
を満たすRectangleオブジェクトがあるべきではないと言っているわけではありませんが、その方法でRectangleオブジェクトを定義する場合、それを拡張するときは、コントラクトがまだ有効であることを確認する必要があります。 LSPに違反しています。
この例には2つの落とし穴があると思います。
最初のものはすでにコメントで指摘されています:内部構造に基づく継承が使用されており、 すべきではありません :オブジェクトのカプセル化を破壊し、壊れやすく、結局手続き型です。
したがって、ソリューションは次のようにサブタイピングを使用しているようです:
interface IRectangle
{
public function setWidth($width);
public function setHeight($height);
}
class Rectangle implements IRectangle
{
private $width;
private $height;
public function setWidth($width)
{
$this->width = $width;
}
public function setHeight($height)
{
$this->height = $height;
}
}
class Square implements IRectange
{
private $side;
public function setWidth($width)
{
$this->side = $width;
}
public function setHeight($height)
{
$this->side = $height;
}
}
しかし、同じ契約違反があります。
これが2番目の落とし穴です。 このコンテキストでは、オブジェクトの責任によって特徴付けられ、2つの異なるインターフェイス、IRectangle
とISquare
で表される2つの異なる抽象化が必要です。
したがって、この悪名高い例は論理的なパズルであり、論理的なエラーはあるが、どこにあるのか誰にもわからないもののようです。 「四角形は長方形の特殊なケースです」と言う根拠は何ですか?それはどのような状況で特別なケースですか?正方形が長方形のように機能する正確な使用例は何ですか?おそらくいくつかはありますが、高さが同じままで長方形の幅が変更されるものではありません。したがって、まずこの愚かな例は常識に違反し、次にOOPに違反し、その後になって初めてLSPに違反します。