タスクエンティティのリストと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置換原理 の違反と見なすことができますか?
はい、それは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();
}
}
_
はい。これは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);
}
}
Liskov置換の原則では、基本クラスは、プログラムの望ましいプロパティを変更することなく、そのサブクラスで置き換えることができるとされています。 ProjectTask
だけが閉じたときに例外を発生させるため、ProjectTask
をTask
の代わりに使用する場合は、プログラムを変更してそれに対応する必要があります。 違反です
しかし、その署名でTask
を変更してmayを変更すると、閉じたときに例外が発生し、原則に違反しなくなります。
LSP違反には3つの当事者が必要です。タイプT、サブタイプS、およびTを使用するが、Sのインスタンスが与えられるプログラムP。
あなたの質問はT(タスク)とS(ProjectTask)を提供しましたが、Pは提供していません。したがって、あなたの質問は不完全であり、答えは限定されます:例外を予期しないPが存在する場合、そのPにはLSPがあります違反。すべてのPが例外を予期している場合、LSP違反はありません。
ただし、do[〜#〜] srp [〜#〜] 違反があります。タスクの状態を変更できること、およびpolicy特定の状態の特定のタスクshouldを他の状態に変更できないことは、2つの非常に異なる責任です。
これら2つの責任は、さまざまな理由で変化するため、別々のクラスに属すべきです。タスクは、タスクであるという事実と、タスクに関連付けられたデータを処理する必要があります。 TaskStatePolicyは、特定のアプリケーションでタスクが状態から状態に遷移する方法を処理する必要があります。
これはかもしれないしそうでないかもしれませんは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()
を使用した場合に得られるもの)を文書化します。また、サブクラスの設計者は、メソッドをオーバーライドしない場合、どの動作が継承されるかを示します。
これで、次の関係が成立するはずです。
リスコフ代替原則の違反ではありません。
リスコフ代替原則は次のように述べています。
q(x)をオブジェクトについて証明可能なプロパティxタイプ[〜#〜] t [〜#〜]とします。 [〜#〜] s [〜#〜]を[〜#〜] t [〜#〜]のサブタイプにする。タイプ[〜#〜] s [〜#〜]オブジェクトyタイプ[〜#〜] s [〜#のオブジェクトの場合、Liskov代入原理に違反します〜]が存在するため、q(y)は証明できません。
サブタイプの実装がリスコフ代入原則の違反ではない理由は非常に単純です。Task::Close()
が実際に何をしているのかについては何も証明できません。もちろん、ProjectTask::Close()
は_Status == Status.Started
_の場合に例外をスローしますが、Task::Close()
の場合は_Status = Status.Closed
_がスローされる可能性があります。
はい、それは違反です。
階層を逆にすることをお勧めします。すべてのTask
がクローズ可能でない場合、close()
はTask
に属しません。おそらく、CloseableTask
以外のすべてのインターフェースが実装できるProjectTasks
が必要です。
LSPの問題であることに加えて、例外を使用してプログラムフローを制御しているようです(この些細な例外をどこかでキャッチし、アプリをクラッシュさせるのではなく、カスタムフローを実行すると想定する必要があります)。
これは、TaskStateのStateパターンを実装し、状態オブジェクトに有効な遷移を管理させるのに適した場所のようです。
LSPとDesign by Contractに関連する重要なことをここで見逃しています-前提条件では、前提条件が満たされていることを確認する責任があるのは呼び出し元です。 DbC理論では、呼び出されたコードは前提条件を検証するべきではありません。コントラクトは、いつタスクを閉じることができるか(CanCloseがTrueを返すなど)を指定する必要があり、呼び出しコードは、Close()を呼び出す前に前提条件が満たされていることを確認する必要があります。
はい、それはLSPの明らかな違反です。
一部の人々は、サブクラスが例外をスローできることを基本クラスで明示的にすることでこれを許容できると主張しますが、私はそれが本当だとは思いません。基本クラスのドキュメントの内容やコードの移動先の抽象化レベルに関係なく、「開始済みプロジェクトタスクを閉じることができません」という部分を追加するため、サブクラスの前提条件が強化されます。これは回避策で解決できるものではなく、LSPに違反しない別のモデルが必要です(または「前提条件を強化できない」制約を緩和する必要があります)。
この場合にLSP違反を回避したい場合は、デコレータパターンを試すことができます。うまくいくかもしれませんが、わかりません。