回答:
LSP(最近聞いたポッドキャストでボブおじさんから与えられた)を示す優れた例は、自然言語で正しく聞こえるものがコードでまったく機能しない場合があることです。
数学では、a Square
はRectangle
です。実際、これは長方形の特殊化です。「is a」はこれを継承でモデル化したい場合に使用します。ただし、コード内でSquare
から派生させた場合Rectangle
、は、Square
期待する場所であればどこでも使用できるはずですRectangle
。これは奇妙な振る舞いをします。
基本クラスにSetWidth
とSetHeight
メソッドがあると想像してくださいRectangle
。これは完全に論理的なようです。あなたの場合はRectangle
、参照が指されSquare
、その後、SetWidth
およびSetHeight
1を設定すると、それに合わせて他の変更になるので意味がありません。この場合Square
、リスコフ置換テストは失敗し、継承Rectangle
を行うという抽象化は悪いものです。Square
Rectangle
Y'allは、他の貴重なSOLID Principles Motivational Postersをチェックする必要があります。
Square.setWidth(int width)
に実装されている場合、なぜ問題になるでしょうthis.width = width; this.height = width;
か?この場合、幅が高さに等しいことが保証されます。
リスコフ代替原則(LSP、 lsp)は、オブジェクト指向プログラミングの概念で、次のように述べられています。
基本クラスへのポインターまたは参照を使用する関数は、それを知らなくても派生クラスのオブジェクトを使用できる必要があります。
中心となるLSPは、インターフェイスとコントラクト、およびクラスを拡張するタイミングと、構成などの別の戦略を使用して目的を達成するタイミングを決定する方法についてです。
この点を説明するために私が見た最も効果的な方法は、Head First OOA&Dでした。彼らは、戦略ゲームのフレームワークを構築するプロジェクトの開発者であるシナリオを提示します。
これらは、次のようなボードを表すクラスを提示します。
すべてのメソッドは、X座標とY座標をパラメーターとして取り、の2次元配列内のタイル位置を特定しますTiles
。これにより、ゲーム開発者はゲームの進行中にボード内のユニットを管理できます。
この本は、ゲームフレームワークが3Dゲームボードもサポートしてフライトを伴うゲームに対応する必要があると言うために、要件を変更し続けます。したがって、ThreeDBoard
拡張するクラスが導入されますBoard
。
一見すると、これは良い決断のようです。とプロパティのBoard
両方を提供し、Z軸を提供します。Height
Width
ThreeDBoard
内訳は、から継承された他のすべてのメンバーを見るとわかりますBoard
。方法はAddUnit
、GetTile
、GetUnits
というように、すべてのXおよびYパラメータの両方を取るBoard
クラスが、ThreeDBoard
井戸としてZパラメータを必要とします。
したがって、これらのメソッドをZパラメータを使用して再度実装する必要があります。ZパラメータはBoard
クラスにコンテキストを持たず、クラスから継承されたメソッドはBoard
その意味を失います。ThreeDBoard
クラスをその基本クラスとして使用しようとするコードのユニットは、Board
非常に不運になります。
多分私たちは別のアプローチを見つける必要があります。代わりに拡張でBoard
、ThreeDBoard
で構成されなければならないBoard
オブジェクト。Board
Z軸の単位ごとに1 つのオブジェクト。
これにより、カプセル化や再利用などの適切なオブジェクト指向の原則を使用でき、LSPに違反しません。
代替可能性は、オブジェクト指向プログラミングの原則であり、コンピュータプログラムでは、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{}
Bird bird
。フライを使用するには、オブジェクトをFlyingBirdsにキャストする必要がありますが、これは適切ではありませんか?
Bird bird
それは使用できませんfly()
。それでおしまい。aを渡してDuck
もこの事実は変わりません。クライアントがを持っている場合、FlyingBirds bird
それが渡されても、Duck
常に同じように動作するはずです。
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
。
ロバート・マーティンは、リスコフ代替原則に関する優れた論文を持っています。原則に違反する可能性のある微妙な方法とそれほど微妙ではない方法について説明します。
論文のいくつかの関連部分(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
関数を継承します。Square
正方形の幅と高さは同じであるため、これらの関数はにはまったく不適切です。これは、デザインに問題があることを示す重要な手がかりとなるはずです。ただし、問題を回避する方法があります。オーバーライドしてSetWidth
、SetHeight
[...]ただし、次の関数を検討してください。
void f(Rectangle& r) { r.SetWidth(32); // calls Rectangle::SetWidth }
Square
この関数にオブジェクトへの 参照を渡すSquare
と、高さが変更されないため、オブジェクトが破損します。これは明らかにLSPの違反です。この関数は、その引数の導関数に対しては機能しません。[...]
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。
LSPは、いくつかのコードは、それが型のメソッドを呼び出していると考えて、必要な場合でありT
、かつ無意識のタイプのメソッドを呼び出すことができるS
、S 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
と同じか、スーパータイプでなければなりません。。Ti
T
共分散とは、分散が継承と同じ方向であることを意味します。つまり、So
サブタイプの各メソッドの出力の型は、スーパータイプの対応するメソッドの対応する出力の型S
と同じか、サブタイプでなければなりません。To
T
これは、呼び出し元が型を持っている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%カバーできる一般的なモデルは決してないということです。知識が存在するためには、予想外の可能性が多く存在します。つまり、無秩序とエントロピーは常に増加している必要があります。これがエントロピー力です。潜在的な拡張のすべての可能な計算を証明するには、すべての可能な拡張をアプリオリに計算します。
これが停止定理が存在する理由です。つまり、チューリング完全プログラミング言語で可能なすべてのプログラムが終了するかどうかは決定できません。特定のプログラムが終了することを証明できます(すべての可能性が定義および計算されているプログラム)。しかし、そのプログラムの拡張の可能性がチューリング完全でない場合(たとえば、依存型付けによるもの)でない限り、そのプログラムのすべての可能な拡張が終了することを証明することは不可能です。チューリング完全性の基本的な要件は無限再帰であるため、ゲーデルの不完全性定理とラッセルのパラドックスが拡張にどのように適用されるかを理解することは直感的です。
これらの定理の解釈は、エントロピー力の一般化された概念的な理解にそれらを組み込みます。
すべての回答に長方形と正方形があり、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 !
}
}
サブタイプは、同じ結果を生成しないため、同じ方法で使用できなくなりました。
Database::selectQuery
サポートされている SQLのサブセットのみをサポートするようにのセマンティクスを制限している限り、この例はLSPに違反しません。それはほとんど実用的ではありません...つまり、この例は、ここで使用されている他のほとんどの例よりもさらに簡単に理解できます。
Liskovに違反しているかどうかを判断するためのチェックリストがあります。
チェックリスト:
履歴の制約:メソッドをオーバーライドする場合、基本クラスの変更不可能なプロパティを変更することはできません。これらのコードを見てみると、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#開発者です)では不可能なので、私はそれらについて気にしません。
参照:
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を満たします。
2 + "2"
)。「強く型付けされた」と「静的に型付けされた」を混同しているのではないでしょうか。
ロング親クラスを拡張する際にストーリー短い、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/
基本クラスへのポインターまたは参照を使用する関数は、それを知らなくても派生クラスのオブジェクトを使用できる必要があります。
私が最初にLSPについて読んだとき、これは非常に厳密な意味であると想定し、本質的にそれをインターフェースの実装とタイプセーフなキャストと同等と見なしました。これは、LSPが言語自体によって保証されるかどうかを意味します。たとえば、この厳密な意味では、コンパイラに関する限り、ThreeDBoardは確かにBoardの代わりに使用できます。
LSPは一般にそれよりも広く解釈されることがわかりましたが、コンセプトについてさらに読んだ後。
つまり、ポインタの背後にあるオブジェクトがポインタ型ではなく派生型であることをクライアントコードが「認識する」とは、タイプセーフに限定されないということです。LSPへの準拠は、オブジェクトの実際の動作を調べることでテストすることもできます。つまり、オブジェクトの状態とメソッド引数がメソッド呼び出しの結果、またはオブジェクトからスローされた例外のタイプに与える影響を調べます。
もう一度例に戻ると、理論的には、BoardメソッドをThreeDBoardで問題なく機能させることができます。ただし実際には、ThreeDBoardが追加しようとしている機能を妨害することなく、クライアントが適切に処理できない動作の違いを防ぐことは非常に困難です。
この知識があれば、LSPアドヒアランスの評価は、コンポジションが継承ではなく既存の機能を拡張するためのより適切なメカニズムであるかどうかを判断するための優れたツールになります。
誰もがLSPが技術的に何であるかをカバーしたと思います:あなたは基本的にサブタイプの詳細から抽象化し、スーパータイプを安全に使用したいです。
したがって、Liskovには3つの基本的なルールがあります。
署名ルール:構文的には、サブタイプのスーパータイプのすべての操作の有効な実装が必要です。コンパイラがチェックできるもの。スローする例外の数を少なくし、少なくともスーパータイプのメソッドと同じようにアクセスできるようにするための小さなルールがあります。
メソッドルール:これらの操作の実装は、意味的に適切です。
プロパティルール:これは、個々の関数呼び出しを超えています。
これらのプロパティはすべて保持する必要があり、追加のサブタイプ機能がスーパータイププロパティに違反してはなりません。
これらの3つの点が処理されると、基礎となるものから抽象化され、疎結合コードを記述していることになります。
出典:Javaでのプログラム開発-Barbara Liskov
LSP の使用の重要な例は、ソフトウェアのテストです。
BのLSP準拠サブクラスであるクラスAがある場合、Bのテストスイートを再利用してAをテストできます。
サブクラスAを完全にテストするには、おそらくいくつかのテストケースを追加する必要がありますが、少なくともスーパークラスBのすべてのテストケースを再利用できます。
これを実現する方法は、McGregorが「テスト用の並列階層」と呼ぶものを構築ATest
することですBTest
。私のクラスはから継承します。テストケースがタイプBではなくタイプAのオブジェクトで機能するようにするには、何らかの注入が必要です(単純なテンプレートメソッドパターンで実行できます)。
すべてのサブクラス実装でスーパーテストスイートを再利用することは、実際にはこれらのサブクラス実装がLSP準拠であることをテストする方法であることに注意してください。したがって、サブクラスのコンテキストでスーパークラステストスイートを実行する必要があると主張することもできます。
Stackoverflowの質問への回答もご覧ください。「インターフェイスの実装をテストするために、一連の再利用可能なテストを実装できますか?」
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の定式化は強すぎます。
タイプSの各オブジェクトo1にタイプTのオブジェクトo2があり、Tに関して定義されたすべてのプログラムPについて、o1がo2に置き換えられた場合、Pの動作は変更されず、SはTのサブタイプです。
これは基本的に、SがTとまったく同じものの完全にカプセル化された別の実装であることを意味します。そして、私は大胆で、パフォーマンスがPの動作の一部であると判断することができます...
したがって、基本的に、遅延バインディングの使用はLSPに違反します。ある種類のオブジェクトを別の種類のオブジェクトに置き換えるときに別の動作を取得することがOOの要です!
プロパティはコンテキストに依存し、必ずしもプログラム全体の動作を含むわけではないため、ウィキペディアで引用されている公式の方が優れています。
非常に単純な文で、次のように言うことができます。
子クラスは、基本クラスの特性に違反してはなりません。それに対応できなければなりません。サブタイピングと同じです。
リスコフの代替原則(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の拡張に過ぎず、新しい派生クラスが基本クラスを、その動作を変更せずに拡張していることを確認する必要があることを意味します。
参照:オープンクローズの原則
より良い構造のためのいくつかの同様の概念:構成に関する規約
補遺:
派生クラスが従わなければならない基本クラスのInvariant、前提条件、事後条件について誰も書かなかったのはなぜでしょうか。派生クラスDが基本クラスBによって完全に支持されるためには、クラスDは特定の条件に従う必要があります。
したがって、派生クラスは、基本クラスによって課せられた上記の3つの条件を認識している必要があります。したがって、サブタイピングのルールは事前に決定されています。つまり、「IS A」の関係は、サブタイプによって特定のルールが守られている場合にのみ守られます。これらのルールは、不変条件、前提条件、事後条件の形式で、正式な「設計契約」によって決定される必要があります。
これに関するさらなる議論は私のブログで利用できます:Liskov Substitution principal
簡単に言えば、LSPは、同じスーパークラスのオブジェクトは何も壊すことなく互いに交換できる必要があると述べています。
たとえば、Cat
とDog
クラスから派生したAnimal
クラスがある場合、Animalクラスを使用するすべての関数は、正常に使用Cat
またはDog
動作できるはずです。
ボードの配列の観点からThreeDBoardを実装することは、それほど有用でしょうか?
おそらく、さまざまな面でThreeDBoardのスライスをボードとして扱うことをお勧めします。その場合、ボードのインターフェース(または抽象クラス)を抽象化して、複数の実装を可能にすることができます。
外部インターフェースに関しては、TwoDBoardとThreeDBoardの両方のボードインターフェースを除外することをお勧めします(ただし、上記の方法はいずれも適合しません)。
正方形は、幅と高さが等しい長方形です。正方形が幅と高さの2つの異なるサイズを設定する場合、正方形は不変条件に違反します。これは、副作用を導入することによって回避されます。しかし、四角形が前提条件0 <高さ、0 <幅のsetSize(height、width)を持っている場合。派生サブタイプメソッドには高さ==幅が必要です。より強力な前提条件(そしてそれはlspに違反します)。これは、正方形は長方形ですが、前提条件が強化されているため、有効なサブタイプではないことを示しています。回避策(一般的に悪いこと)は副作用を引き起こし、これによりポスト条件が弱まります(これはlspに違反します)。ベースのsetWidthのポスト条件は0 <幅です。派生は高さ==幅でそれを弱めます。
したがって、サイズ変更可能な正方形は、サイズ変更可能な長方形ではありません。
この原理は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という別の違反が発生します。実際、派生クラスの正方形を作成すると、基本クラスの長方形が変更されます。
私がこれまでに見つけたLSPの最も明確な説明は、「リスコフ置換の原則によると、派生クラスのオブジェクトは、システムにエラーを引き起こしたり、基本クラスの動作を変更したりすることなく、基本クラスのオブジェクトを置き換えることができるはずです。 " ここから。この記事では、LSPに違反して修正するためのコード例を示します。
コードで長方形を使用するとします
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);
}
}
我々は交換した場合Rectangle
にSquare
私たちの最初のコードでは、それが解除されます:
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に違反すると、ある時点でコードにエラーが発生する可能性があります。
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();
}
私はあなたが記事を読むことをお勧めします:Liskov Substitution Principle(LSP)に違反しています。
そこには、リスコフ置換の原則とは何か、すでに違反している場合に推測するのに役立つ一般的な手がかり、およびクラス階層をより安全にするのに役立つアプローチの例があります。
LISKOV SUBSTITUTION PRINCIPLE(Mark Seemannの本より)は、クライアントまたは実装のいずれかを壊すことなく、インターフェイスのある実装を別の実装に置き換えることができるはずであると述べています。今日はそれらを予測します。
コンピューターを壁から抜いた場合(実装)、壁のコンセント(インターフェース)もコンピューター(クライアント)も故障しません(実際、それがラップトップコンピューターの場合、一定の時間バッテリーで動作することもできます)。 。ただし、ソフトウェアの場合、クライアントはサービスが利用可能であることを期待することがよくあります。サービスが削除された場合、NullReferenceExceptionが発生します。このような状況に対処するために、「何もしない」インターフェースの実装を作成できます。これはNull Object [4]として知られる設計パターンであり、コンピューターを壁から外すことにほぼ対応しています。疎結合を使用しているので、実際の実装を問題を引き起こさずに何もしないものに置き換えることができます。
Likovの置換原則は、プログラムモジュールがBaseクラスを使用している場合、プログラムモジュールの機能に影響を与えることなく、Baseクラスへの参照をDerivedクラスに置き換えることができると述べています。
意図-派生型は、その基本型を完全に置換できる必要があります。
例-Javaの共変の戻り値の型。
以下は、この記事からの抜粋で、わかりやすく説明しています。
[..]いくつかの原則を理解するためには、いつ違反されたかを理解することが重要です。これは私が今やることです。
この原則の違反とはどういう意味ですか?これは、オブジェクトが、インターフェースで表現された抽象化によって課された規約を満たさないことを意味します。つまり、抽象化を間違って識別したことになります。
次の例を考えてみます。
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に違反します」と言います。ただし、そうではありません—子が親の契約に違反しない限り。