リスコフの代替原則に違反した場合、何が問題になる可能性がありますか?


27

私は、リスコフ代替原理の違反の可能性について、この非常に投票された質問に従っていました。Liskov Substitutionの原則が何であるかは知っていますが、開発者としてオブジェクト指向コードを書いている間にこの原則を考えないと、何が間違っているのかが私の心ではまだはっきりしていません。


6
LSPに従わない場合、何が問題になる可能性がありますか?最悪のシナリオ:あなたはCode-thulhuを呼び出すことになります!;)
FrustratedWithFormsDesigner

1
その元の質問の著者として、私はそれがかなり学術的な質問であったことを付け加えなければなりません。違反はコードにエラーを引き起こす可能性がありますが、LSPの違反に帰することができる重大なバグやメンテナンスの問題は一度もありません。
ポールTデイビス

2
@Paulだから、複雑なオブジェクト指向階層(これは自分で設計しなかったが、拡張する必要があるかもしれない)により、基本クラスの目的について不確かな人々によって契約が左右に壊れたため、プログラムに問題はなかったそもそも?私はあなたがうらやましい!:)
アンドレスF.

@PaulTDaviesは、ユーザー(ライブラリを使用するプログラマー)がライブラリの実装に関する詳細な知識を持っているかどうか(つまり、ライブラリのコードにアクセスし、それに精通しているかどうか)に依存します。最終的に、ユーザーは何十もの条件付きチェックを行うか、ラッパーを構築します非LSP(クラス固有の動作)を説明するためのライブラリの周り。ライブラリがクローズドソースの商用製品である場合、最悪のシナリオが発生します。
rwong

@Andresとrwong、これらの問題を答えで説明してください。認められた答えはPaul Daviesをほぼサポートしています。優れたコンパイラー、静的アナライザー、または最小単位テストがある場合、結果はすぐに気づき、修正されます。
-user949300

回答:


31

私はそれが非常によく投票された理由の一つであるその質問で非常によく述べられていると思う

タスクでClose()を呼び出すと、開始されたステータスのProjectTaskの場合は呼び出しが失敗する可能性がありますが、ベースタスクの場合は失敗しません。

想像してみてください:

public void ProcessTaskAndClose(Task taskToProcess)
{
    taskToProcess.Execute();
    taskToProcess.DateProcessed = DateTime.Now;
    taskToProcess.Close();
}

このメソッドでは、.Close()呼び出しがときどき爆発するため、派生型の具体的な実装に基づいて、Taskにサブタイプがない場合のこのメソッドの記述方法からこのメソッドの動作を変更する必要がありますこのメソッドに渡されました。

liskov置換違反のため、型を使用するコードは、派生型の内部動作を明示的に認識して、それらを異なる方法で処理する必要があります。これにより、コードが緊密に結合され、一般的に実装を一貫して使用することが難しくなります。


つまり、子クラスは、親クラスで宣言されていない独自のパブリックメソッドを持つことはできませんか?
松o

@Songo:必ずしもそうではありません:可能ですが、これらのメソッドはベースポインター(または参照または変数、または使用する言語が呼び出すもの)から "到達不能"であり、オブジェクトの型を照会するためのランタイム型情報が必要です。これらの関数を呼び出す前に。しかし、これは言語の構文とセマンティクスに強く関連する問題です。
エミリオ

2
いいえ。これは、子クラスが親クラスの型であるかのように参照される場合です。この場合、親クラスで宣言されていないメンバーはアクセスできません。
歯ごたえガムボール

1
@Phil Yep; これは密結合の定義です。あるものを変更すると、他のものも変更されます。疎結合クラスでは、外部のコードを変更せずに実装を変更できます。これが、契約が優れている理由であり、オブジェクトの消費者に変更を要求しない方法を案内します。契約を満たせば、消費者は変更を必要とせず、したがって疎結合が達成されます。消費者が契約ではなく実装にコーディングする必要がある場合、これは密結合であり、LSPに違反する場合に必要です。
ジミー・ホッファ

1
@ user949300仕事を達成するためのソフトウェアの成功は、その品質、長期、または短期コストの尺度ではありません。設計原則は、ソフトウェアを「機能させる」ためではなく、ソフトウェアの長期的なコストを削減するためのガイドラインをもたらす試みです。人々は、実用的なソリューションの実装に失敗しながら、必要なすべての原則に従うか、何も従わずに実用的なソリューションを実装できます。Javaコレクションは多くの人にとって有効かもしれませんが、それは長期的にJavaコレクションを使用するためのコストが可能な限り安価であることを意味しません。
ジミーホファ

13

基本クラスで定義されたコントラクトを満たさない場合、結果がオフになると黙って失敗する可能性があります。

ウィキペディアの州のLSP

  • サブタイプでは前提条件を強化できません。
  • サブタイプでは事後条件を弱めることはできません。
  • スーパータイプの不変式は、サブタイプに保存する必要があります。

これらのいずれかが保持されない場合、発信者は予期しない結果を得る可能性があります。


1
これを実証する具体的な例はありますか?
マークブース

1
@MarkBooth円楕円/四角形の問題は、それを実証するのに役立ちます。ウィキペディアの記事は、開始するのに適した場所です。en.wikipedia.org
エドヘイスティングス

7

インタビューの質問の年代記から古典的な例を考えてみましょう。あなたは楕円から円を導き出しました。どうして?もちろん、円はIS-AN楕円だからです!

を除く...楕円には2つの機能があります。

Ellipse.set_alpha_radius(d)
Ellipse.set_beta_radius(d)

サークルの半径は均一なので、明らかに、これらはサークルに対して再定義する必要があります。次の2つの可能性があります。

  1. set_alpha_radiusまたはset_beta_radiusを呼び出した後、両方が同じ量に設定されます。
  2. set_alpha_radiusまたはset_beta_radiusを呼び出した後、オブジェクトは円ではなくなります。

ほとんどのOO言語は2番目の言語をサポートしていませんが、それには十分な理由があります。CircleがCircleでなくなったことは驚くべきことです。したがって、最初のオプションが最適です。ただし、次の機能を検討してください。

some_function(Ellipse byref e)

some_functionがe.set_alpha_radiusを呼び出すと想像してください。しかし、eは実際には円であったため、驚くべきことにベータ半径も設定されています。

そして、ここに代替原理があります。サブクラスはスーパークラスの代替である必要があります。そうでなければ、驚くべきことが起こります。


1
可変オブジェクトを使用すると、問題が発生する可能性があると思います。円も楕円です。ただし、円でもある楕円を別の楕円に置き換えると(セッターメソッドを使用して行うことです)、新しい楕円も円になるという保証はありません(円は楕円の適切なサブセットです)。
ジョルジオ

2
純粋に機能的な世界(不変オブジェクトを使用)では、メソッドset_alpha_radius(d)は、楕円型と楕円型の両方の円型を返します。
ジョルジオ

@Giorgioはい、この問題は可変オブジェクトでのみ発生することを述べたはずです。
カズドラゴン

@KazDragon:楕円が円ではないことがわかっているのに、だれかが楕円を円オブジェクトに置き換えるのはなぜですか?誰かがそれを行うと、モデル化しようとしているエンティティを正しく理解できません。しかし、この置換を許可することにより、ソフトウェアでモデル化しようとしている基礎となるシステムの理解がゆるやかになり、実際に悪いソフトウェアが作成されるのではないでしょうか?
マーベリック

@maverickあなたが私が逆に記述した関係を読んだと思います。提案されているis-a関係は、その逆です:円は楕円です。具体的には、円は、アルファ半径とベータ半径が同一の楕円です。そのため、楕円をパラメーターとして期待するすべての関数が等しく円をとることが期待されるかもしれません。calculate_area(Ellipse)を検討してください。それに円を渡すと同じ結果が得られます。しかし問題は、楕円の突然変異関数の振る舞いがCircleのそれらの代わりにならないことです。
カズドラゴン

6

素人の言葉で:

コードには非常に多くのCASE / switch句があります。

これらのCASE / switch句のすべてに、時々追加される新しいケースが必要になります。つまり、コードベースは、本来あるべきほどスケーラブルで保守可能ではありません。

LSPを使用すると、コードをハードウェアのように機能させることができます。

新しい外部スピーカーのペアを購入したため、iPodを変更する必要はありません。古い外部スピーカーと新しい外部スピーカーの両方が同じインターフェースを尊重しているため、iPodは必要な機能を失うことなく交換できます。


2
-1:すべての悪い答え
トーマスエディング

3
@トーマス私は同意しません。それは良い例えです。彼は、期待を破らないことについて話しています。それがLSPの目的です。(ケース/スイッチ約部分は少し弱いものの、同意)
アンドレスF.

2
そして、Appleはコネクタを変更してLSPを破りました。この答えは生き続けています。
メイガス14

switchステートメントがLSPにどのような関係があるのか​​わかりません。typeof(someObject)「許可」することを決定するために切り替えを参照している場合は、確かですが、それは完全に別のアンチパターンです。
サラ

switchステートメントの量の大幅な削減は、LSPの望ましい副作用です。オブジェクトは同じインターフェイスを拡張する他のオブジェクトを表すことができるため、特別なケースを処理する必要はありません。
Tulainsコルドバ

1

javaのUndoManagerを使用して実際の例を示す

AbstractUndoableEdit2つの状態(元に戻すおよびやり直し)があることを指定するコントラクトから継承し、1つの呼び出しundo()redo()

ただし、UndoManagerにはより多くの状態があり、元に戻すバッファーのように動作します(すべての編集ではなくundo一部の呼び出しを元に戻し、事後条件を弱めます)

これは、UndoManagerをCompoundEditに追加してから呼び出すと、CompoundEditでend()undoを呼び出すとundo()、編集が部分的に取り消されたままになると、各編集で呼び出すという仮想的な状況になります。

それUndoManagerを避けるために自分でロールバックしました(おそらく名前を変更する必要がUndoBufferあります)


1

例:UIフレームワークを使用しており、Control基本クラスをサブクラス化して独自のカスタムUIコントロールを作成します。Control基本クラスは、メソッド定義されなければならない、ネストされたコントロールのコレクションを(もしあれば)を返すを。しかし、メソッドをオーバーライドして、実際にアメリカ合衆国大統領の誕生日のリストを返すようにします。getSubControls()

これで何がうまくいかないのでしょうか?コントロールのリストが期待どおりに返されないため、コントロールのレンダリングが失敗することは明らかです。ほとんどの場合、UIがクラッシュします。あなたはされている契約破りコントロールのサブクラスをを遵守することが期待されます。


0

モデリングの観点から見ることもできます。クラスのインスタンスがクラスのインスタンスでAもあると言うときB、「クラスのインスタンスの観測可能な動作は、クラスのインスタンスのA観測可能な動作としても分類できる」ことを意味しますB(これは、クラスBがクラスA。)

したがって、LSPに違反するということは、設計に矛盾があることを意味します。オブジェクトに対していくつかのカテゴリを定義しているのに、実装でそれらを尊重していないのであれば、何かが間違っているに違いありません。

「このボックスには青いボールのみが含まれています」というタグを付けてボックスを作成し、そこに赤いボールを投げるようなものです。間違った情報を表示する場合、そのようなタグの使用は何ですか?


0

最近、いくつかの主要な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、多くの場所でインデックスおよびフロー制御変数として使用されます。上記のクラスは、生の形式と派生した形式の両方でコードベース全体に散らばっています。この場合、リスコフ置換の原則に違反するということは、基本クラスをとる各関数へのすべての呼び出しのコンテキストを知る必要があるということです。

コード用途の両方のオブジェクトClass AClass B私は単純に作ることができないので、Class A使用に人々を強制的に抽象的なClass B

動作するいくつかの非常に有用なユーティリティ関数と、動作するClass A他の非常に有用なユーティリティ関数がありますClass B。理想的には私が上で動作することができます任意のユーティリティ関数を使用できるようにしたいと思いClass A上をClass B。aをとる関数の多くは、LSP違反ではない場合Class B、簡単にa をとることができますClass A

これについて最悪なのは、アプリケーション全体がこれら2つのクラスに依存し、常に両方のクラスで動作し、これを変更すると100の方法で壊れるので、この特定のケースはリファクタリングが本当に難しいことです(これを行うつもりですとにかく)。

これを修正するには、NameTranslatedプロパティを作成する必要があります。これは、プロパティのClass BバージョンになりName、派生Nameプロパティへのすべての参照を慎重に変更して、新しいNameTranslatedプロパティを使用します。ただし、これらの参照の1つでも間違っていると、アプリケーション全体が爆破する可能性があります。

コードベースには単体テストがないため、これは開発者が直面する可能性のある最も危険なシナリオに近いものです。違反を変更しないと、各メソッドでどのタイプのオブジェクトが操作されているかを追跡するために膨大な精神エネルギーを費やす必要があり、違反を修正すると、不適切な時間に製品全体が爆発する可能性があります。


派生クラス内で、同じ名前(ネストされたクラスなど)を持つ別の種類の継承プロパティをシャドウし、新しい識別子BaseNameを作成しTranslatedName、クラスAスタイルNameとクラスBの両方の意味にアクセスするとどうなりますか?その後Name、型の変数にアクセスしようとするBと、コンパイラエラーで拒否されるため、すべての参照が他の形式のいずれかに変換されたことを確認できます。
supercat

私はその場所で働いていません。修正するのは非常に面倒でした。:
スティーブン14年

-4

LSPに違反する問題を感じたい場合は、ベースクラスの.dll / .jarのみ(ソースコードなし)があり、新しい派生クラスを構築する必要がある場合はどうなるかを考えてください。このタスクを完了することはできません。


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