web-dev-qa-db-ja.com

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

タスクエンティティのリストと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である場合、呼び出しが失敗する可能性があり、ベースタスクである場合は失敗します。 しかし、これはビジネス要件です。失敗するはずです。これは Liskov置換原理 の違反と見なすことができますか?

137
Paul T Davies

はい、それはLSPの違反です。 Liskov置換原則が必要

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

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

強化された前提条件を継承階層の最上位レベルにすることで、これを修正できます。

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

Close()trueを返す状態でのみCanClose()の呼び出しが有効であることを規定することにより、次のようにTaskに前提条件を適用します。 ProjectTaskと同様に、LSP違反を修正します。

_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();
    }
}
_
178
dasblinkenlight

はい。これは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);
    }
}
85
Euphoric

Liskov置換の原則では、基本クラスは、プログラムの望ましいプロパティを変更することなく、そのサブクラスで置き換えることができるとされています。 ProjectTaskだけが閉じたときに例外を発生させるため、ProjectTaskTaskの代わりに使用する場合は、プログラムを変更してそれに対応する必要があります。 違反です

しかし、その署名でTaskを変更してmayを変更すると、閉じたときに例外が発生し、原則に違反しなくなります。

24

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

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

ただし、do[〜#〜] srp [〜#〜] 違反があります。タスクの状態を変更できること、およびpolicy特定の状態の特定のタスクshouldを他の状態に変更できないことは、2つの非常に異なる責任です。

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

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

22
Robert Martin

これはかもしれないしそうでないかもしれませんはLSPの違反です。

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

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

コードの問題は、タイプTaskのオブジェクトがどのように動作することが期待されるかを文書化していないことです。コードを書きましたが、契約はありません。 _Task.Close_の契約を追加します。追加する契約に応じて、_ProjectTask.Close_のコードはLSPに従うか、または従わない。

Task.Closeの次のコントラクトがあるとすると、_ProjectTask.Close_ しないのコードはLSPに従います。

_     // 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_ doesのコードは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のサブタイプ(またはTと等しい)である場合、Sのコードの動作は、文書化されたTの動作を改善する必要があります。
  • SがTのサブタイプ(またはTと等しい)である場合、Sのデフォルトの動作は、文書化されたTの動作を改善する必要があります。
  • クラスのコードの実際の動作は、ドキュメント化されたデフォルトの動作を改善する必要があります。
16

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

リスコフ代替原則は次のように述べています。

q(x)をオブジェクトについて証明可能なプロパティxタイプ[〜#〜] t [〜#〜]とします。 [〜#〜] s [〜#〜][〜#〜] t [〜#〜]のサブタイプにする。タイプ[〜#〜] s [〜#〜]オブジェクトyタイプ[〜#〜] s [〜#のオブジェクトの場合、Liskov代入原理に違反します〜]が存在するため、q(y)は証明できません。

サブタイプの実装がリスコフ代入原則の違反ではない理由は非常に単純です。Task::Close()が実際に何をしているのかについては何も証明できません。もちろん、ProjectTask::Close()は_Status == Status.Started_の場合に例外をスローしますが、Task::Close()の場合は_Status = Status.Closed_がスローされる可能性があります。

6
Oswald

はい、それは違反です。

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

4
Tom G

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

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

3
Ed Hastings

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

1
Ezoela Vacca

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

一部の人々は、サブクラスが例外をスローできることを基本クラスで明示的にすることでこれを許容できると主張しますが、私はそれが本当だとは思いません。基本クラスのドキュメントの内容やコードの移動先の抽象化レベルに関係なく、「開始済みプロジェクトタスクを閉じることができません」という部分を追加するため、サブクラスの前提条件が強化されます。これは回避策で解決できるものではなく、LSPに違反しない別のモデルが必要です(または「前提条件を強化できない」制約を緩和する必要があります)。

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

0
inf3rno