web-dev-qa-db-ja.com

リスコフ代替原則-前提条件の強化

それが本当に何を意味するのか、私は少し混乱しています。関連する質問( これはリスコフ代入原則の違反ですか? )で、この例は明らかに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(); 
          }
     }
}
7
John V

状況によります。

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の記事 を参照してください。

6
Doc Brown

一部のコードがコード自体によってLSPに違反しているかどうかを判断することはできません。 契約を知っている必要があります各メソッドが満たすこと。

この例では明示的なコントラクトが指定されていませんがあるため、Close()メソッドの意図されたコントラクトが何であるかを推測する必要があります。

Close()メソッドの基本クラス実装を見ると、そのメソッドの唯一の効果は、その後のStatusが_Status.Closed_であることです。このメソッドの契約についての私の推測は次のとおりです。

Statusを_Status.Closed_にするために必要なことをすべて実行します。

しかし、それは単なる妥当な推測です。明記されていない場合、誰もそのことを確信することはできません。

当たり前のことだと思いましょう。

オーバーライドされたClose()メソッドもその契約を満たしますですか?このメソッドを実行した後、_Status.Closed_になる可能性は2つあります。

  • メソッドを呼び出す前に、すでに_Status.Closed_がありました。
  • _Status.Started_がありました。次に、基本実装を呼び出して、フィールドを_Status.Closed_に設定します。
  • その他の場合はすべて、別のステータスになります。

Statusに2つの可能な値ClosedStartedしかない場合(たとえば、2値の列挙型)、すべてが問題なく、常に_Status.Closed_ Close()メソッドの後。

しかし、おそらくより多くの可能なStatus値があり、最終的にStatusは_Status.Closed_ではなく、したがって契約違反になります。


OPは「基本クラスを使用しているところならどこでも、その派生クラスを使用できる」という有名な文について尋ねました。

それについて詳しく説明したいと思います。

私はそれを「基本クラスを使用しているところはどこでもそのコントラクト内で、その派生クラスを使用できますそのコントラクトに違反することなく

したがって、コンパイルエラーを生成しないこと、またはエラーをスローせずに実行することだけでなく、契約で要求されることを行うについても問題です。

そして、それは私がクラスに意図された操作の範囲内にある何かをするように頼む状況にのみ適用されます。したがって、乱用の状況(前提条件が満たされていない場合など)を気にする必要はありません。


あなたの質問をもう一度読んだ後、私はその文脈で多態性に関する段落を追加するべきだと思います。

ポリモーフィズムとは、異なるクラスのインスタンスの場合、同じメソッド呼び出しによって異なる実装が実行されることを意味します。したがって、ポリモーフィズムを使用すると、技術的にはClose()メソッドをオーバーライドすることができます。ストリームを開きます。技術的にはそれは可能ですが、ポリモーフィズムの悪い使い方です。そして、ポリモーフィズムの良い使い方と悪い使い方に関する1つの原則はLSPです。

21
Ralf Kleberhoff

リスコフ代替原則は、すべて contracts についてです。これは、前提条件(対応する動作を実行するためにtrueを保持する必要がある条件)、事後条件(動作がジョブを完了したと見なすためにtrueを保持する必要がある条件)、不変条件(前、実行中、終了後にtrueを保持する必要がある条件)で構成されます対応するメソッドの実行)と履歴の制約(私の意見では、これは不変のサブセットなので、ウィキペディアを確認する方がよいでしょう)。質問で、Taskクラスのimpliedコントラクトにリンクすると、次のようになります。

  • 前提条件:なし
  • 事後条件:Statusclosed
  • 不変:何も見えない

したがって、子クラスのいずれかがタスクを閉じない場合、LSP違反と見なされます特定の契約内

ただし、「startedの場合にのみタスクを閉じる」のように契約を明示的に仮定している場合は、問題ありません。あなたのコードでそれを行うことができます-その例はこれです 受け入れられた答え 。しかし、ほとんどの場合それはできません-プレーンなコメントを使うことができます。

基本的に、LSP違反について考えるときはいつでも、あなたはすでに契約に精通しているはずです。 「LSP違反」というものはなく、「ある契約内のLSP違反」だけです。

7
Zapadlo

はい、まだ違反(おそらく)

Taskの一部のクライアントは、「after Task::Close()Status is now Closed」に依存し、ProjectTaskを検出するとブレークします。あなたは現在そのようなクライアントを持っていないかもしれませんが、Task::Close()の事後条件は "Statusでなければなりませんは有効ですが指定されていない状態にあります」とは、基本的に無意味です。

より自然なのは、Task::Close()が後置条件 "Status is Closed"を持つことです。これにより、ProjectTaskの実装が無効になります。

これはvoid DoStuff()メソッドの主要な問題です。すべての副作用があるため、これらの副作用に依存しているものがあります。 bool TryClose()は、「Close()可能であれば、それについて教えてください」という意味です。

1
Caleth

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()とは異なり、コントラクトが満たされていれば、サブクラスは任意の実装を提供できます。

1
StuartLC

はい、それはまだLSPの違反です。

ベースTaskクラスでは、Close()が呼び出された後、ステータスはClosedになります。
派生したProjectTaskクラスで、呼び出し後Close()ステータスはClosed

したがって、事後条件(ステータスはClosed)はProjectTaskクラスでは満たされなくなりました。
または言い換えると、タスクについてのみ知っているクライアントは、Close()を呼び出した後、ステータスがClosedであることに依存する場合があります。彼にProjectTaskを "偽装"したタスク(許可されている)として与え、彼がClose()を呼び出すと、結果が異なります(ステータスはではない可能性があります)閉まっている)。

0
CharonX