リスク置換の原則と契約

この記事のアイデアは、Alexander Byndyの記事「 LSPの補足 」から着想を得ており、Alexanderの記事の詳細な解説と見なすことができます。



したがって、次の質問では、チームメンバーの1人がTクラスのDoubleList TインターフェイスのIListを実装しようとしているため、 Addメソッドを使用して要素を追加するときに、1つではなく2つの同一の要素が追加されます。 Tクラスのリスト 常に1つの要素のみを追加するため、この動作はLiskov Substitution Principle(LSP)に違反すると想定できます。



次に、これが.NETプラットフォームの場合であるかどうかを確認し、このメソッドが何をすべきかの正式な仕様がない場合に、このメソッドがLiskの置換原則に違反していると主張できるかどうかについて話しましょう。







最初に、問題の状態を少し編集する必要があります(はい、これは公平ではないことを理解していますが、この場合は正当化されます)。 元のタスクでは、 DoubleListクラスによるTインターフェイスのIList 実装について説明しましたが、実際には、.NET Frameworkでは、 AddメソッドはTインターフェイスのICollection 宣言されています。





コードを読みやすくして例を実行するために、 DoubleListクラスの一般化されたバージョンを使用するのではなく、文字列を格納するための特殊なコンテナー、つまり 実際、 DoubleList文字列インターフェイスのICollection 実装します。



public class DoubleList : ICollection<string> { private readonly List<string> _backingList = new List<string>(); public void Add(string item) { //     ,   2  _backingList.Add(item); _backingList.Add(item); } //     }
      
      







ここで、コードのどこかで文字列インターフェイスのICollectionを使用し(つまり、ポリモーフィックに使用している)、要素の数が厳密に1増加することを想定するとします。 このような動作は、正式な事後条件(たとえば、コードコントラクトを使用)を使用して表現するか、単体テストとして表現できます。 後で契約に移るので、最初に単体テストを見てみましょう。



 [Test] public void TestAddMethodAddsOnlyOneElement() { ICollection<string> collection = new DoubleList(); int oldCount = collection.Count; collection.Add("foo"); Assert.That(collection.Count, Is.EqualTo(oldCount + 1)); }
      
      







はい、確かに、 文字列の コレクション リスト オブジェクトとして使用する場合、このテストは正常に機能し、 DoubleListを使用するとクラッシュします。



しかし、1つだけ質問があります。TインターフェイスのICollectionの Addメソッドからこの動作を正確に期待できますか? 正確性(つまり、バグであるかどうか、およびコードまたはデザインのどこでもかまいません)に関しては、コードの動作が仕様に準拠しているかどうかによってのみ決定されます。 仕様は正式なものでも非正式なものでもかまいませんが、プログラムは意図したことをやめない場合にのみ不正であり、誰もそれが何のためであるかを知らなければ、誰もそれが正しく動作しないと言うことはできません。



x = y / 2などの一部の式は、それ自体では正しくも正しくもありません。 それはすべて、 xyの関係がどうあるかに依存します(詳細については、 「契約による設計。ソフトウェアの正確性」を参照してください)。 Addメソッドの場合も同様です。このメソッドが何をすべきかについての公式または少なくとも非公式の説明がなければ、相続人によって正しく実装されているかどうかはわかりません。 ICollection(Of T).Add Methodの公式ドキュメントを開くと、このメソッドがコレクションに要素を追加するという1行だけが表示されます。 同時に、それ以外のすべて(たとえば、正確に追加するか、数量を追加するか)はあくまでも仮定であり、できることはTCollectionのICollectionの他の実装がどのように動作するかを調べることだけです



次のテストを追加しますが、今回は、2つの同一の要素を追加すると要素の数が2増加することを確認します。これは、1つの要素を追加するよりも特別な場合であることは明らかです。 Addメソッドのドキュメントに戻りますが、同じ要素を追加できないという言葉はありません。 唯一の制限は、アイテムを読み取り専用コレクションに追加しないことです。 他の前提条件はありません(公式のドキュメントでは通常例外として表されます)、ドキュメントには含まれていません。つまり、 Addメソッドを呼び出す前に他のチェックを行う必要はありません。



 [TestCaseSource("GetAllCollections")] public void TestAddToCollectionTwiceAddsTwoElement(ICollection<string> collection) { int oldCount = collection.Count; if (collection.IsReadOnly) { Console.WriteLine("Current collection type ({0}) is readonly. Skipping it...", collection.GetType()); return; } collection.Add("foo"); collection.Add("foo"); Assert.That(collection.Count, Is.EqualTo(oldCount + 2)); }
      
      







繰り返しになりますが、 Addメソッドの前提条件を確認することに注意してください 。契約は1ゴールのゲームではないため、呼び出されたコードが独自のコードを満たすように契約の一部を果たす必要があります。 テスト自体はパラメーター化され(NUnitから)、その入力値はGetAllCollectionsメソッドから取得されます。 TインターフェースのICollectionを実装する既存のクラスをすべて探す必要はありませんが、人気のあるコレクションタイプを手動で追加するだけです



 private static ICollection<string>[] GetAllCollections() { return new ICollection<string>[] { new List<string>(), new HashSet<string>(), new Collection<string>(), new BindingList<string>(), new LinkedList<string>(), new ObservableCollection<string>(), new ReadOnlyCollection<string>(new List<string>()), new SortedSet<string>(), new string[]{}, }; }
      
      







次に、上記のテストを実行して、少なくとも2種類のコレクションが1つの要素のみを追加することを確認します(2つの追加を要求しました)。 もちろん、これら Tの HashSet Tの SortedSetなど、重複を保存できないコレクションタイプです



さて、元の質問に戻りましょう。Add メソッドの実装は、 Lisk 置換の原則で DoubleList クラス に違反し ますか? 回答: いいえ、壊れません!



TインターフェースのICollectionAddメソッドは、 追加後にコレクションに含まれる要素の数に制限を課しません。つまり、このインターフェースを実装するすべてのクラスに特定のルールに従うことを要求できません。 実際、( Tの ICollection 実装するコレクションの動作に基づいて)より望ましいのは、次の前提条件です: Addメソッドを呼び出した後、 Containsメソッドへの次の呼び出しはtrueを返し、コレクション内の要素の数は減少しません(つまり、newCount> = oldCount)。



この場合、テストで既存のステートメントを次のように置き換えれば:



 Assert.IsTrue(collection.Contains("foo"));
      
      







その後、すべてのテストが成功します。



おわりに


DoubleListは、 Liskの置換の原則に違反していない、またはBCLの標準コレクションクラスと共に違反していると安全に言うことができます。 一方、 DoubleListクラスの設計 「不正」であることに同意しますが、知恵で理解することはほとんど不可能な原則の違反のためではなく(*)、そのような動作が直感的に不明確であるという事実にむしろ訴えますそして、必ず護衛の問題につながります。



Meyerが厚手の本(追加リンクを参照)で、文(前提条件、事後条件、および不変式)を使用してプログラムの正式な仕様にこのような重要な役割を割り当てているのも当然です。 メソッドが何をすべきかについての正式な説明がないため、「正しく」動作する相続人を書くことが難しくなります。「正しい」ことは誰にも言えないからです。 メソッドシグネチャ(および可能なコメント)は、基本クラスの代わりに相続人を使用するときに動作が変更されるかどうかを理解するにはあまりにも非公式です。

次回は、 コード コントラクト (サポートはmscorlibに部分的に含まれています)が問題の解決にどのように役立つか、 Addメソッドが持つ正式な事後条件の種類を確認します。



-



(*)テキストに直接脚注を書くという通常の規則から離れ、リスコフの代用の原則の定義に関する別のサブセクションを作成します。



代替リスコフの原理。 定義


James Copleyenの注目すべき本、C ++プログラミングは、リスコフ置換原理の優れた、完全に理解できない定義を提供します。



...タイプSのすべてのオブジェクトo1に対して、タイプTのオブジェクトo2が存在し、Tのコンテキストで定義されたすべてのプログラムPについて、o1がo2に置き換えられたときにPの動作が変わらない場合、SはTの基本タイプです



一般に、「 Tのコンテキストで何を意味するのかが完全に不明確であり、この動作がどこにも記述されていない場合、「 Pの 動作は変わらない」ということはまったく明確ではないことを除いて、すべてがうまくいきます。



別の人気がありますが、これ以上理解できない定義は、 Bob Martinの本にあります。



ベースタイプのサブタイプを代用するのは可能であるべきです。



この定義は単純ですが、ほとんど明確ではありません。 すべてのオブジェクト指向プログラミング言語では、継承から基本クラスへの暗黙的な変換があります(オープン継承の使用を考慮に入れる)。したがって、この要件は、ユーザー定義クラスではなく、コンパイラーまたはプログラミング言語に起因する必要があります。 Martin自身の本(およびThe Liskov Substituion Principleの記事)で、契約の重要性、特に仮想メソッドの動作を指定するための前提条件と事後条件の重要性について語っています。



契約-これは、 P動作を指定する可能性です。

これは元の定義に記載されており、継承者の動作が基本クラスまたはインターフェースの動作に対応することを保証(または少なくとも理解)できるのは、この仕様のおかげです。



サイトリンク




  1. バートランド・マイヤー。 ソフトウェアシステムのオブジェクト指向設計

  2. ロバートC.マーティン。 C#でのアジャイル開発の原則、パターン、および手法#

  3. ロバートC.マーティンリスコフ代替原理

  4. ロバートC.マーティンデザインの原則とパターン

  5. 契約による設計。 ソフトウェアの正確性について

  6. 契約による設計。 継承



All Articles