それが本当に何を意味するのか、私は少し混乱しています。関連する質問( これはリスコフ代入原則の違反ですか? )で、この例は明らかにLSPに違反していると言われていました。
しかし、新しい例外がスローされない場合でも、それは違反でしょうか?では、単純に多態性ではないですか?つまり:
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)
{
base.Close();
}
}
}
状況によります。
LSPを検証するには、Close
関数の正確な規約を知る必要があります。コードが次のようになっている場合
public class Task
{
// after a call to this method, the status must become "Closed"
public virtual void Close()
//...
}
次に、このコメントを無視する派生クラスはLSPに違反します。ただし、コードが次のようになっている場合
public class Task
{
// tries to close the the task
// (check Status afterwards to find out if it has worked)
public virtual void Close()
//...
}
その場合、ProjectTask
はLSPに違反しません。
ただし、コメントがない場合、Close
のような関数名により、IMHOは呼び出し側にステータスを「Closed」に設定するという明確な期待を与え、関数がそのように機能しない場合は、少なくとも「驚きの原則」に違反している。
また、Eiffelのような一部のプログラミング言語には、コントラクトの言語サポートが組み込まれているため、必ずしもコメントに依存する必要はありません。リストについては このWikipediaの記事 を参照してください。
一部のコードがコード自体によってLSPに違反しているかどうかを判断することはできません。 契約を知っている必要があります各メソッドが満たすこと。
この例では明示的なコントラクトが指定されていませんがあるため、Close()
メソッドの意図されたコントラクトが何であるかを推測する必要があります。
Close()メソッドの基本クラス実装を見ると、そのメソッドの唯一の効果は、その後のStatus
が_Status.Closed
_であることです。このメソッドの契約についての私の推測は次のとおりです。
Status
を_Status.Closed
_にするために必要なことをすべて実行します。
しかし、それは単なる妥当な推測です。明記されていない場合、誰もそのことを確信することはできません。
当たり前のことだと思いましょう。
オーバーライドされたClose()
メソッドもその契約を満たしますですか?このメソッドを実行した後、_Status.Closed
_になる可能性は2つあります。
Status.Closed
_がありました。Status.Started
_がありました。次に、基本実装を呼び出して、フィールドを_Status.Closed
_に設定します。Status
に2つの可能な値Closed
とStarted
しかない場合(たとえば、2値の列挙型)、すべてが問題なく、常に_Status.Closed
_ Close()
メソッドの後。
しかし、おそらくより多くの可能なStatus
値があり、最終的にStatus
は_Status.Closed
_ではなく、したがって契約違反になります。
OPは「基本クラスを使用しているところならどこでも、その派生クラスを使用できる」という有名な文について尋ねました。
それについて詳しく説明したいと思います。
私はそれを「基本クラスを使用しているところはどこでもそのコントラクト内で、その派生クラスを使用できますそのコントラクトに違反することなく。
したがって、コンパイルエラーを生成しないこと、またはエラーをスローせずに実行することだけでなく、契約で要求されることを行うについても問題です。
そして、それは私がクラスに意図された操作の範囲内にある何かをするように頼む状況にのみ適用されます。したがって、乱用の状況(前提条件が満たされていない場合など)を気にする必要はありません。
あなたの質問をもう一度読んだ後、私はその文脈で多態性に関する段落を追加するべきだと思います。
ポリモーフィズムとは、異なるクラスのインスタンスの場合、同じメソッド呼び出しによって異なる実装が実行されることを意味します。したがって、ポリモーフィズムを使用すると、技術的にはClose()
メソッドをオーバーライドすることができます。ストリームを開きます。技術的にはそれは可能ですが、ポリモーフィズムの悪い使い方です。そして、ポリモーフィズムの良い使い方と悪い使い方に関する1つの原則はLSPです。
リスコフ代替原則は、すべて contracts についてです。これは、前提条件(対応する動作を実行するためにtrueを保持する必要がある条件)、事後条件(動作がジョブを完了したと見なすためにtrueを保持する必要がある条件)、不変条件(前、実行中、終了後にtrueを保持する必要がある条件)で構成されます対応するメソッドの実行)と履歴の制約(私の意見では、これは不変のサブセットなので、ウィキペディアを確認する方がよいでしょう)。質問で、Task
クラスのimpliedコントラクトにリンクすると、次のようになります。
Status
はclosed
したがって、子クラスのいずれかがタスクを閉じない場合、LSP違反と見なされます特定の契約内。
ただし、「started
の場合にのみタスクを閉じる」のように契約を明示的に仮定している場合は、問題ありません。あなたのコードでそれを行うことができます-その例はこれです 受け入れられた答え 。しかし、ほとんどの場合それはできません-プレーンなコメントを使うことができます。
基本的に、LSP違反について考えるときはいつでも、あなたはすでに契約に精通しているはずです。 「LSP違反」というものはなく、「ある契約内のLSP違反」だけです。
はい、まだ違反(おそらく)
Task
の一部のクライアントは、「after Task::Close()
、Status
is now Closed
」に依存し、ProjectTask
を検出するとブレークします。あなたは現在そのようなクライアントを持っていないかもしれませんが、Task::Close()
の事後条件は "Status
でなければなりませんは有効ですが指定されていない状態にあります」とは、基本的に無意味です。
より自然なのは、Task::Close()
が後置条件 "Status
is Closed
"を持つことです。これにより、ProjectTask
の実装が無効になります。
これはvoid DoStuff()
メソッドの主要な問題です。すべての副作用があるため、これらの副作用に依存しているものがあります。 bool TryClose()
は、「Close()
可能であれば、それについて教えてください」という意味です。
Ralfや他の人が述べたように、想定されている「常識」規約以外は、実際にコードにコントラクトを実装または適用していません。 Close()
は、サブクラスに追加されたコメント以外は、オブジェクトを閉じた状態にしておく必要があります。
私の意見では、あなたが提供した例(私はそれが a関連する投稿 からコピーされていることを知っています)には、Close()
メソッドをvirtualベースのTask
クラス-これは、デフォルトの実装を提供している場合でも、他のユーザーをサブクラスTask
に招待して動作を変更するだけです契約を遵守します。
さらに悪いことに、Status
はまったくカプセル化されていないため、状態はパブリックに変更可能であり、Close
に関するコントラクトは、どのイベントでも状態を外部にランダムに割り当てることができるため、かなり無意味です。
したがって、クラス階層がClose
のポリモーフィックな動作を必要としない場合、_Task.Close
_のvirtual
キーワードを削除するだけです。
_// Encapsulate status, to control state transition
public Status Status { get; private set; }
public void Close()
{
Status = Status.Closed;
}
_
(他の状態遷移についても同じことを行います)
ただし、ポリモーフィックな動作が必要な場合(つまり、サブクラスがClose
のカスタム実装を提供する必要がある場合)、ベースのTask
クラスをインターフェイスに変換し、事前条件と事後条件を適用します次のように Code Contracts を介して:
_[ContractClass(typeof(TaskContracts))]
public interface ITask
{
Status Status { get; } // No externally accessible set
void Close();
// Other transition methods here.
}
_
対応する契約で:
_[ContractClassFor(typeof(ITask))]
public class TaskContracts : ITask
{
public Status Status { get; }
public void Close()
{
Contract.Requires(Status != Status.Closed, "Already Closed!");
Contract.Ensures(Status == Status.Closed, "Must close Task on Completion!");
}
}
_
このアプローチの利点は、インターフェースの使用規約が明確(かつ強制可能)であり、バイパスできるvirtual Close()
とは異なり、コントラクトが満たされていれば、サブクラスは任意の実装を提供できます。
はい、それはまだLSPの違反です。
ベースTaskクラスでは、Close()が呼び出された後、ステータスはClosedになります。
派生したProjectTaskクラスで、呼び出し後Close()ステータスはClosed 。
したがって、事後条件(ステータスはClosed)はProjectTaskクラスでは満たされなくなりました。
または言い換えると、タスクについてのみ知っているクライアントは、Close()を呼び出した後、ステータスがClosedであることに依存する場合があります。彼にProjectTaskを "偽装"したタスク(許可されている)として与え、彼がClose()を呼び出すと、結果が異なります(ステータスはではない可能性があります)閉まっている)。