私は、リスコフの代替原則の違反の可能性について、これを 非常に投票された質問 に従っていました。私はリスコフ置換の原則が何であるかを知っていますが、私の心の中でまだはっきりしていないのは、開発者として私がオブジェクト指向のコードを書く際に原則について考えなければ何がうまくいかないのかです。
私はそれが非常によく投票された理由の1つであるその質問で非常によく述べられていると思います。
タスクでClose()を呼び出すと、開始ステータスのProjectTaskの場合、ベースタスクの場合と異なり、失敗する可能性があります。
次のことを想像してみてください。
public void ProcessTaskAndClose(Task taskToProcess)
{
taskToProcess.Execute();
taskToProcess.DateProcessed = DateTime.Now;
taskToProcess.Close();
}
このメソッドでは、時々.Close()呼び出しが爆発するので、派生型の具体的な実装に基づいて、Taskにサブタイプがない場合のこのメソッドの記述方法から、このメソッドの動作を変更する必要があります。このメソッドに渡されます。
Liskov置換違反のため、型を使用するコードは、それらを異なる方法で処理するために、派生型の内部動作を明確に知っている必要があります。これはコードを密結合し、一般的に実装を一貫して使用することを難しくします。
基本クラスで定義されているコントラクトを満たさない場合、オフの結果を取得したときに、何も通知されずに失敗する可能性があります。
これらのいずれかが成立しない場合、呼び出し元は予期しない結果を得る可能性があります。
インタビューの質問の記録からの古典的なケースを考えてみましょう。EllipseからCircleを派生させました。どうして?もちろん円IS-AN楕円だからです!
例外...楕円には2つの機能があります。
Ellipse.set_alpha_radius(d)
Ellipse.set_beta_radius(d)
明らかに、Circleの半径は均一であるため、これらはCircleに対して再定義する必要があります。次の2つの可能性があります。
ほとんどのOO言語は2番目の言語をサポートしていませんが、それには十分な理由があります。あなたのサークルがもうサークルではないことを発見するのは驚くべきことです。したがって、最初のオプションが最善です。しかし、次の関数:
some_function(Ellipse byref e)
Some_functionがe.set_alpha_radiusを呼び出すとします。しかし、eはreally a Circleだったので、驚くことにベータ半径も設定されています。
そして、ここに置換の原則があります。サブクラスはスーパークラスの代わりに使用できなければなりません。そうでなければ、驚くべきことが起こります。
素人の言葉で:
あなたのコードには、ひどいたくさんのCASE/switch句があります。
これらすべてのCASE/switch句は、新しいケースを時々追加する必要があります。つまり、コードベースは、本来のスケーラビリティと保守性を備えていません。
LSPにより、コードはハードウェアのように機能します:
新しい外部スピーカーのペアを購入したため、iPodを変更する必要はありません。古い外部スピーカーと新しい外部スピーカーの両方が同じインターフェイスを尊重しているため、iPodが必要な機能を失うことなく交換できます。
例:UIフレームワークを使用していて、Control
基本クラスをサブクラス化して独自のカスタムUIコントロールを作成します。 Control
基本クラスは、メソッドgetSubControls()
を定義します。これは、ネストされたコントロール(存在する場合)のコレクションを返すshouldです。しかし、メソッドをオーバーライドして、実際に米国大統領の生年月日のリストを返します。
これで何がうまくいかないのでしょうか?期待どおりにコントロールのリストを返さないため、コントロールのレンダリングが失敗することは明らかです。ほとんどの場合、UIがクラッシュします。あなたは契約を破る Controlのサブクラスが遵守することが期待されています。
JavaのUndoManager を使用して実際の例を示す
AbstractUndoableEdit
から継承し、その契約では2つの状態(元に戻すとやり直し)があることが指定されており、undo()
とredo()
を1回呼び出すだけで状態を切り替えることができます
ただし、UndoManagerにはより多くの状態があり、元に戻すバッファーのように機能します(undo
を呼び出すたびに一部のが元に戻されますが、編集は元に戻されません、事後条件が弱められます)。
これにより、UndoManagerをend()
を呼び出す前にCompoundEditに追加し、そのCompoundEditでundoを呼び出すと、編集内容が部分的に取り消されたまま、編集ごとにundo()
が呼び出されるという架空の状況が発生します。
それを避けるために自分のUndoManager
をロールしました(ただし、名前をUndoBuffer
に変更する必要があります)
最近、いくつかの主要なLiskov違反者がいるコードベースを継承しました。重要なクラスで。これは私に多大な苦痛をもたらしました。その理由を説明させてください。
Class A
から派生したClass B
があります。 Class A
とClass B
は、独自の実装でClass A
がオーバーライドする一連のプロパティを共有します。 Class A
プロパティを設定または取得することは、Class B
からまったく同じプロパティを設定または取得することとは異なる効果があります。
public Class A
{
public virtual string Name
{
get; set;
}
}
Class B : A
{
public override string Name
{
get
{
return TranslateName(base.Name);
}
set
{
base.Name = value;
FunctionWithSideEffects();
}
}
}
これが.NETで翻訳を行うにはひどい方法であるという事実を別にして、このコードには他にも多くの問題があります。
この場合、Name
はいくつかの場所でインデックスおよびフロー制御変数として使用されます。上記のクラスは、未加工のフォームと派生したフォームの両方でコードベース全体に散らばっています。この場合のLiskov置換の原則に違反するということは、基本クラスをとる各関数へのすべての呼び出しのコンテキストを知る必要があるということです。
コードはClass A
とClass B
の両方のオブジェクトを使用しているため、Class A
を抽象化してClass B
を使用するように強制することはできません。
Class A
を操作する非常に便利なユーティリティ関数と、Class B
を操作する他の非常に便利なユーティリティ関数があります。理想的には、Class A
でClass B
を操作できるユーティリティ関数をすべて使用できるようにしたいと考えています。 Class B
を使用する関数の多くは、LSPの違反がなければ、Class A
を簡単に使用できます。
これについて最悪のことは、アプリケーション全体がこれら2つのクラスに依存し、常に両方のクラスで動作するため、この特定のケースをリファクタリングすることが本当に難しいことです。とにかく)。
これを修正するために私がしなければならないことは、NameTranslated
プロパティのClass B
バージョンであるName
プロパティを作成し、派生へのすべての参照を非常に慎重に変更することですName
プロパティを使用して、新しいNameTranslated
プロパティを使用します。ただし、これらの参照の1つでも誤って取得すると、アプリケーション全体が爆発する可能性があります。
コードベースにはユニットテストがないため、これは開発者が直面する可能性のある最も危険なシナリオにかなり近いものです。違反を変更しない場合、各メソッドで操作されているオブジェクトのタイプを追跡するために大量の精神的エネルギーを費やす必要があり、違反を修正すると、製品全体が不都合なときに爆発する可能性があります。
モデリングの観点からも見ることができます。クラスA
のインスタンスがクラスB
のインスタンスでもあると言う場合、「クラスA
のインスタンスの監視可能な動作も監視可能なものとして分類できるクラスB
"のインスタンスの動作(これは、クラスB
がクラスA
よりも限定的でない場合にのみ可能です。)
したがって、LSPに違反するということは、設計に矛盾があることを意味します。オブジェクトに対していくつかのカテゴリを定義していて、実装でそれらを尊重していない場合、何かが間違っているはずです。
「このボックスには青いボールのみが含まれています」というタグの付いたボックスを作成してから、赤いボールをその中に投げ入れます。間違った情報を表示する場合、そのようなタグの使用は何ですか?