これは、リスコフ代替原則の違反ですか?


132

TaskエンティティのリストとProjectTaskサブタイプがあるとします。タスクはいつでも閉じることができますが、タスクのProjectTasksステータスが[開始済み]になると閉じることができません。UIは、開始済みを閉じるオプションがProjectTask使用できないことを確認する必要がありますが、ドメインにはいくつかの保護手段があります。

public class Task
{
     public Status Status { get; set; }

     public virtual void Close()
     {
         Status = Status.Closed;
     }
}

public class ProjectTask : Task
{
     public override void Close()
     {
          if (Status == Status.Started) 
              throw new Exception("Cannot close a started Project Task");

          base.Close();
     }
}

これClose()で、タスクを呼び出すときに、開始タスクのProjectTask状態の場合は呼び出しが失敗する可能性がありますが、ベースのタスクの場合は失敗しません。しかし、これはビジネス要件です。失敗するはずです。これは、リスコフ代替原理の違反とみなすことができますか?


14
liskov置換に違反するTの例に最適です。ここでは継承を使用しないでください。大丈夫です。
ジミー・ホッファ

8
次のように変更できpublic Status Status { get; private set; }ます。それ以外の場合、Close()メソッドは回避できます。
仕事

5
たぶんそれはこの例にすぎないかもしれませんが、LSPに準拠することに実質的な利点はないと思います。私にとって、質問のこのソリューションは、LSPに準拠するソリューションよりも明確で、理解しやすく、保守しやすいものです。
ベン・リー

2
@BenLee保守は簡単ではありません。このように見えるのは、これが単独で表示されるためです。システムが大きい場合、サブタイプがTask多相コードに奇妙な非互換性を導入しないようにすることTaskは、大したことです。LSPは気まぐれではありませんが、大規模システムの保守性を高めるために正確に導入されました。
アンドレスF.

8
@BenLeeあなたがするTaskCloserプロセスがあると想像してくださいclosesAllTasks(tasks)。このプロセスは明らかに例外をキャッチしようとしません。結局のところ、それはの明示的な契約の一部ではありませんTask.Close()。これでProjectTask、突然TaskCloser(おそらく未処理の)例外がスローされ始めます。これは大したことです!
アンドレスF.

回答:


173

はい、それはLSPの違反です。リスコフの代替原則では

  • サブタイプでは前提条件を強化できません。
  • サブタイプでは事後条件を弱めることはできません。
  • スーパータイプの不変式は、サブタイプに保存する必要があります。
  • 履歴制約(「履歴ルール」)。オブジェクトは、メソッド(カプセル化)によってのみ変更可能と見なされます。サブタイプはスーパータイプには存在しないメソッドを導入する可能性があるため、これらのメソッドの導入により、スーパータイプでは許可されないサブタイプの状態変更が可能になる場合があります。これは履歴制約により禁止されています。

この例では、Close()メソッドを呼び出すための前提条件を強化することにより、最初の要件を破ります。

強化された前提条件を継承階層の最上位に持ってくることで修正できます:

public class Task {
    public Status Status { get; set; }
    public virtual bool CanClose() {
        return true;
    }
    public virtual void Close() {
        Status = Status.Closed;
    }
}

の呼び出しが返さClose()れるときの状態でのみ有効であることを規定することにより、前提条件をに加えてに適用し、LSP違反を修正します。CanClose()trueTaskProjectTask

public class ProjectTask : Task {
    public override bool CanClose() {
        return Status != Status.Started;
    }
    public override void Close() {
        if (Status == Status.Started) 
            throw new Exception("Cannot close a started Project Task");
        base.Close();
    }
}

17
私はそのチェックの複製が好きではありません。Task.Closeに例外をスローして、Closeからvirtualを削除することをお勧めします。
陶酔

4
@Euphoricそれは事実であり、トップレベルCloseにチェックを行わせ、保護されたものを追加するDoCloseことは有効な選択肢です。ただし、OPの例にできるだけ近いものにしたかったのです。それを改善することは別の質問です。
-dasblinkenlight

5
@Euphoric:しかし、今では「このタスクを閉じることはできますか?」という質問に答える方法はありません。閉じようとせずに。これにより、フロー制御に例外が不必要に使用されます。しかし、この種のことは行き過ぎだと認めます。あまりにも遠くに取られて、この種の解決策は、企業の混乱をもたらすことになります。とにかく、OPの質問は原則についてより多くのように私を襲うので、象牙の塔の答えは非常に適切です。+1
ブライアン

30
@Brian The CanCloseはまだあります。タスクを閉じることができるかどうかを確認するために呼び出すことができます。Closeのチェックもこれを呼び出す必要があります。
陶酔

5
@ユーフォリック:ああ、私は誤解しました。あなたは正しい、それははるかにクリーンなソリューションになります。
ブライアン

82

はい。これはLSPに違反します。

私の提案は、CanCloseメソッド/プロパティをベースタスクに追加することです。これにより、どのタスクでも、この状態のタスクを閉じることができるかどうかを判断できます。理由も提供できます。から仮想を削除しますClose

私のコメントに基づいて:

public class Task {
    public Status Status { get; private set; }

    public virtual bool CanClose(out String reason) {
        reason = null;
        return true;
    }
    public void Close() {
        String reason;
        if (!CanClose(out reason))
            throw new Exception(reason);

        Status = Status.Closed;
    }
}

public ProjectTask : Task {
    public override bool CanClose(out String reason) {
        if (Status != Status.Started)
        {
            reason = "Cannot close a started Project Task";
            return false;
        }
        return base.CanClose(out reason);
    }
}

3
このおかげで、あなたはdasblinkenlightの例をさらに1段階進めましたが、私は彼の説明と正当化が好きでした。申し訳ありませんが、2つの回答を受け入れることはできません。
ポールTデイヴィス

署名がパブリック仮想ブールCanClose(out String reason)である理由を知ることに興味があります。または、私が見逃しているより微妙な何かがありますか?
リーチャーギルト

3
@ReacherGilt out / refが何をするかを確認し、コードをもう一度読む必要があると思います。あなたが混乱しています。単に「タスクを終了できない場合、その理由を知りたい」
陶酔

2
outはすべての言語で使用できるわけではなく、タプル(または理由とブール値をカプセル化する単純なオブジェクトを返すと、boolを直接持つことの容易さは失われますが、OO言語間での移植性が向上します。サポートアウト、この答えが間違って何もない。
Newtopian

1
CanCloseプロパティの前提条件を強化しても大丈夫ですか?つまり、条件を追加しますか?
ジョンV

24

Liskovの置換原理では、プログラムの望ましいプロパティを変更せずに、基本クラスをそのサブクラスのいずれかと置き換えることができると規定されています。ProjectTask閉じたときにのみ例外が発生するため、プログラムはそのために変更する必要ProjectTaskがあり、の代わりに使用する必要がありTaskます。違反です。

しかし、閉じたときに例外が発生Taskする可能性があるという署名の記述を変更する場合、原則に違反することはありません。


私はこの可能性があるとは思わないc#を使用していますが、Javaにはあることを知っています。
ポールTデイヴィス

2
@PaulTDaviesスローする例外msdn.microsoft.com/en-us/library/5ast78ax.aspxでメソッドを修飾できます。基本クラスライブラリのメソッドにカーソルを合わせると、例外のリストが表示されます。強制されませんが、それでも発信者に気付かせます。
デスペルター

18

LSP違反には3つの当事者が必要です。タイプT、サブタイプS、およびTを使用しているがSのインスタンスが与えられているプログラムP

あなたの質問はT(タスク)とS(プロジェクトタスク)を提供しましたが、Pは提供しませんでした。したがって、あなたの質問は不完全であり、答えは限定されます。違反。すべてのPが例外を予期している場合、LSP違反はありません。

しかし、あなたが行う必要がありSRP違反を。タスクの状態を変更できるという事実と、特定の状態の特定のタスクを他の状態に変更してはならないというポリシーは、2つの非常に異なる責任です。

  • 責任1:タスクを表します。
  • 責任2:タスクの状態を変更するポリシーを実装します。

これらの2つの責任はさまざまな理由で変化するため、別々のクラスに分類する必要があります。タスクは、タスクであるという事実と、タスクに関連付けられたデータを処理する必要があります。TaskStatePolicyは、特定のアプリケーションでタスクが状態から状態に移行する方法を処理する必要があります。


2
責任はドメインと(この例では)複雑なタスク状態とそのチェンジャーがどれほど複雑かによって大きく異なります。この場合、そのようなことを示すものはないため、SRPには問題はありません。LSP違反に関しては、呼び出し側が例外を予期せず、アプリケーションがエラー状態になるのではなく、合理的なメッセージを表示する必要があるとみなしたと思います。
陶酔

Unca 'ボブは応答しますか?「私たちはふさわしくない!私たちはふさわしくない!」。とにかく... すべてのPが例外を予期する場合、LSP違反はありません。しかし、TインスタンスがOpenTaskException(ヒント、ヒント)をスローできず、すべてのPが例外を予期している場合、実装ではなくインターフェイスへのコードについて何と言っていますか?私は何について話しているのですか?知りません。Unca 'Bobの回答にコメントしているだけでジャズです。
レーダーボブ

3
LSP違反を証明するには3つのオブジェクトが必要であることは正しいです。しかしながら、LSP違反がSの非存在下で正しかった任意のプログラムPが存在する場合に存在するが、Sの添加で失敗
ケビン・クライン

16

これ、LSPの違反である場合とそうでない場合があります。

真剣に。聞いてください。

LSPに従う場合、タイプのProjectTaskオブジェクトは、タイプのオブジェクトがTask動作することが期待されるように動作する必要があります。

コードの問題は、タイプのオブジェクトがどのようTaskに動作するかを文書化していないことです。コードを記述しましたが、契約はありません。の契約を追加しますTask.Close。私が追加した契約に応じて、ProjectTask.CloseLSPに従うかどうかに従わないコードがあります。

Task.Closeの次のコントラクトが与えられた場合、コードはLSPに従いProjectTask.Close ません

     // Behaviour: Moves the task to the closed state
     // and does not throw any Exception.
     // Default behaviour: Moves the task to the closed state
     // and does not throw any Exception.
     public virtual void Close()
     {
         Status = Status.Closed;
     }

Task.Close、のコードのために、次の契約が与えられProjectTask.Close ない LSPに従います。

     // Behaviour: Moves the task to the closed status if possible.
     // If this is not possible, this method throws an Exception
     // and leaves the status unchanged.
     // Default behaviour: Moves the task to the closed state
     // and does not throw any Exception.
     public virtual void Close()
     {
         Status = Status.Closed;
     }

オーバーライドされる可能性のあるメソッドは、2つの方法で文書化する必要があります。

  • 「振る舞い」は、受信者オブジェクトがであるTaskことを知っているが、それが直接のインスタンスであるクラスを知らないクライアントが信頼できるものを文書化します。また、サブクラスの設計者に、どのオーバーライドが妥当であり、どれが妥当でないかを伝えます。

  • 「デフォルトの振る舞い」は、受信者オブジェクトが直接のインスタンスであることを知っているクライアントが信頼できるものを文書化しますTask(つまり、使用すると何を取得しますかnew Task()。メソッドをオーバーライドします。

これで、次の関係が成り立つはずです。

  • SがTのサブタイプである場合、Sの文書化された動作は、Tの文書化された動作を改良する必要があります。
  • SがTのサブタイプ(またはそれに等しい)である場合、Sのコードの動作は、文書化されたTの動作を改善する必要があります。
  • SがTのサブタイプ(またはそれに等しい)である場合、Sのデフォルトの動作は、文書化されたTの動作を改善する必要があります。
  • クラスのコードの実際の動作は、文書化されたデフォルトの動作を改良する必要があります。

@ user61852は、メソッドのシグネチャに例外を発生させることができるという点を挙げており、これを実行するだけで(実際の効果コードがない)、LSPを壊すことはありません。
ポールTデイヴィス

@PaulTDaviesそのとおりです。しかし、ほとんどの言語では、署名はルーチンが例外をスローすることを宣言するのに適した方法ではありません。たとえば、OP(C#の場合)では、2番目の実装Closeがスローされます。そのため、シグネチャは例外スローされる可能性あることを宣言しています- 例外スローされないとは言いません。この点では、Javaのほうが優れています。それでも、メソッドが例外を宣言する可能性があることを宣言する場合、その可能性がある(またはそうする)状況を文書化する必要があります。そのため、LSPに違反しているかどうかを確認するには、署名を超えた文書が必要だと私は主張します。
セオドアノーベル

4
ここでの回答の多くは、契約がわからない場合に契約が有効かどうかわからないという事実を完全に無視しているようです。その答えをありがとう。
gnasher729

良い答えですが、他の答えも同様に良いです。彼らは、そのクラスにはその兆候を示すものがないため、基本クラスは例外をスローしないと推測します。そのため、基本クラスを使用するプログラムは、例外に備えるべきではありません。
inf3rno

あなたは、例外リストがどこかに文書化されるべきであることは正しいです。最適な場所はコード内だと思います。関連する質問がここにあります:stackoverflow.com/questions/16700130/…しかし、アノテーションなどなしでこれを行うことができます...また、if (false) throw new Exception("cannot start")基本クラスに何かを書くだけです。コンパイラはそれを削除しますが、それでもコードには必要なものが含まれています。ところで 前提条件がまだ強化されているため、これらの回避策でまだLSP違反があります
...-inf3rno

6

これは、リスコフ代替原則の違反ではありません。

Liskov Substitution Principleによれば:

ましょうQ(x)はオブジェクトに関する証明可能性であるX型のT。してみましょうSはのサブタイプであることT。タイプSは、オブジェクト場合リスコフの置換原則に違反するY型のSが存在するように、Q(y)が証明可能ではありません。

サブタイプの実装がLiskov Substitution Principleに違反しない理由は非常に単純ですTask::Close()。実際に何が行われるかについては何も証明できません。確かに、ProjectTask::Close()ときに例外がスローされますStatus == Status.Startedが、そうかもしれませんStatus = Status.ClosedTask::Close()


4

はい、違反です。

階層を逆にすることをお勧めします。すべてTaskがクローズ可能でない場合、close()はに属しませんTask。おそらくCloseableTask、すべてProjectTasksが実装できないインターフェースが必要でしょう。


3
すべてのタスクはクローズ可能ですが、すべての状況下ではありません。
ポールTデイヴィス

このアプローチは、すべてのタスクがClosableTaskを実装することを期待するコードを書くかもしれませんが、問題を正確にモデル化するので、私にとって危険です。私はステートマシンが嫌いなので、このアプローチとステートマシンの間で引き裂かれています。
ジミー・ホッファ

Taskそれ自体が実装されていない場合はCloseableTask、を呼び出すためにどこかで安全でないキャストを行っていますClose()
トムG

それは私が怖いものだ@TomG
ジミー・ホッファ

1
すでにステートマシンがあります。間違った状態にあるため、オブジェクトを閉じることができません。
カズ

3

LSPの問題であることに加えて、例外を使用してプログラムフローを制御しているようです(この些細な例外をどこかでキャッチし、アプリをクラッシュさせるのではなく、カスタムフローを実行することを想定する必要があります)。

TaskStateのStateパターンを実装し、状態オブジェクトに有効な遷移を管理させるのに適した場所のようです。


1

ここでは、LSPとDesign by Contractに関連する重要なことを見逃しています-前提条件では、前提条件が満たされていることを確認するのは呼び出し側です。DbC理論では、呼び出されたコードは前提条件を検証すべきではありません。コントラクトは、タスクを閉じることができるタイミングを指定する必要があり(CanCloseがTrueを返すなど)、呼び出しコードは、Close()を呼び出す前に前提条件が満たされていることを確認する必要があります。


契約では、ビジネスに必要な動作を指定する必要があります。この場合、そのClose()は、startedで呼び出されたときに例外を発生させProjectTaskます。これは事後条件(メソッドが呼び出された後に何が起こるか示す)であり、それを満たすことは呼び出されたコードの責任です。
五葉

@Goyoはい、しかし、他の人が言ったように、例外は前提条件を強化し、したがってClose()を呼び出すだけでタスクを閉じる(暗黙の)契約に違反するサブタイプで発生します。
エゾエラヴァッカ

どの前提条件?表示されません。
五葉

@Goyo受け入れられた答えをチェックします。たとえば、:)基本クラスでは、Closeには前提条件がなく、呼び出されてタスクを閉じます。ただし、子には、ステータスが開始されていないという前提条件があります。他の人が指摘したように、これはより強力な基準であり、したがって動作は代替可能ではありません。
エゾエラヴァッカ

気にしないで、質問の前提条件を見つけました。しかし、その後、呼び出されたコードが前提条件をチェックし、それらが満たされない場合に例外を発生させることで、何も悪いことはありません(DbCに関して)。これは「防御プログラミング」と呼ばれます。さらに、この場合のように前提条件が満たされないときに何が起こるかを示す事後条件がある場合、事後条件が満たされることを保証するために、実装は事前条件を検証しなければなりません。
Goyo

0

はい、それはLSPの明らかな違反です。

ここで、サブクラスが例外をスローできることを基本クラスで明示的にすることでこれが受け入れられると主張する人もいますが、それは真実ではないと思います。基本クラスで文書化するもの、またはコードを移動する抽象化レベルに関係なく、サブプロジェクトでは「開始済みプロジェクトタスクを閉じることができません」部分を追加するため、前提条件は引き続き強化されます。これは回避策で解決できるものではありません。LSPに違反しない別のモデルが必要です(または「前提条件を強化できない」制約を緩める必要があります)。

この場合、LSP違反を回避したい場合は、デコレータパターンを試すことができます。うまくいくかもしれませんが、わかりません。

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