インターフェイスの継承とコントラクト

.NETプラットフォームでのコントラクトプログラミングの機能を親切に提供するCode Contractsライブラリは、 Contractクラスの静的メソッドを使用して前提条件と事後条件を設定します。 一方では、属性に基づいた代替実装があまりにも制限されるため、これは良いことです。 一方、これは、本質的にコードを含まないインターフェイスコントラクトまたは抽象メソッドに関して、特定の困難を追加します。つまり、メソッドを呼び出す方法はありません。



これは、インターフェースまたは抽象クラスにハングアップするContractClassAttributeと、コントラクト自体にハングアップするContractClassForAttributeの 2つの属性を使用して解決されます。



/// <summary> /// Custom collection interface /// </summary> [ContractClass(typeof(CollectionContract))] public interface ICollection { void Add(string s); int Count { get; } bool Contains(string s); } /// <summary> /// Contract class for <see cref="ICollection"/>. /// </summary> [ContractClassFor(typeof(ICollection))] internal abstract class CollectionContract : ICollection { public void Add(string s) { Contract.Ensures(Count >= Contract.OldValue(Count)); Contract.Ensures(Contains(s)); } public int Count { get { Contract.Ensures(Contract.Result<int>() >= 0); return default(int); } } [Pure] public bool Contains(string s) { return default(bool); } }
      
      







このICollectionインターフェースの有用性は疑わしいように見えますが、それらの助けにより、インターフェースの継承に関連する契約のすべての必要な機能と制限を見ることができます。 この例の焦点は、 CollectionContractクラスの2つのメンバー、 AddメソッドとCountプロパティです。これらのメソッドは、対応するメソッドの前提条件/事後条件を指定します。



現在、一部のクラスがICollectionインターフェイスを実装し、事後条件に違反している場合、実行時に例外(特定のCONTRACT_FULL文字を含む)として、また場合によってはStatic Checkerによる静的コード分析中にこれが表示されます。



 internal class CustomCollection : ICollection { private readonly List<string> _backingList = new List<string>(); public void Add(string s) { // Ok, we're crazy enough to violate precondition // of ICollection interface if (Contains(s)) _backingList.Remove(s); else _backingList.Add(s); } public int Count { get { // We should add some hints to static checker to eliminate a warning Contract.Ensures(Contract.Result<int>() == _backingList.Count); return _backingList.Count; } } public bool Contains(string s) { return _backingList.Contains(s); } }
      
      







この場合、これはまさに起こることです。静的チェッカーは、 Addメソッドの事後条件が満たされない場合があると判断します(既存の要素を追加するとき、itJを削除します)。 しかし、彼を信じていない場合、実行中に契約違反が発生する可能性があります。



 [Test] public void TestAddTwiceAddsTwoElements() { var collection = new CustomCollection(); int oldCount = collection.Count; collection.Add(""); collection.Add(""); Assert.That(collection.Count, Is.EqualTo(oldCount + 2)); }
      
      







Assertが呼び出されると、このテストは失敗します それ以前は、次の例外使用して、 Addメソッドを再度呼び出そうとすると、 System.Diagnostics.Contracts .__ ContractsRuntime + ContractException:Postcondition failed:Count> = Contract.OldValue(Count)





静的分析は「契約プログラミング」の最も興味深い機能の1つですが、コードコントラクトライブラリのこのことは実際のプロジェクトにはまだ準備ができていないと安全に言えます。 第一に、コンパイル時間は桁違いに長くなる可能性があります(!)、そして第二に、その周りで何が起こっているのかを理解するために、タンバリンで子供っぽくジャンプする必要がありますが、この場合でも困難なケースではほとんど何もできません。 CustomCollectionクラスのような単純な例でさえ、静的プロパティアナライザーが何が起こっているのか理解できず、大量の警告を出すため、 Countプロパティに事後条件を手動で追加する必要がありました。 宣言、文書化、関係の形式化など、契約の他のすべての利点 残りますが、コンパイル時にではなく、実行時に(たとえば、単体テストと連動して)機能します。



前提条件の緩和と事後条件の強化




コントラクトにより、クラスとそのクライアント間だけでなく、クラスとその子孫間の関係も形式化できます。 仮想メソッドの前提条件は、このメソッドを呼び出すために何を行う必要があるかをクライアントに伝え、後条件はこのメソッドが何をするかを伝えます。 さらに、クライアントコードは、それが動作するオブジェクトの動的タイプが何であるかに関係なく、このコントラクトの履行を信頼できます。 これはまさに、 前回お話ししたリスコフ代入原理が語っていることです。



ただし、代用の原則は、クライアントの仮定を「破らない」場合、相続人がメソッドのセマンティクスを変更することを禁止しません。 そのため、継承者でオーバーライドされたメソッドの前提条件は、呼び出し元コードでそれほど厳密ではない可能性があり(より弱い前提条件を含む場合があります)、事後条件はより厳密である可能性があります。 ロシア語からロシア語に翻訳するために、簡単な例を見てみましょう。



 class Base { public virtual object Foo(string s) { Contract.Requires(!string.IsNullOrEmpty(s)); Contract.Ensures(Contract.Result<object>() != null); return new object(); } } class Derived : Base { public override object Foo(string s) { // Now we're requiring empty string Contract.Requires(s != null); // And returning only strings Contract.Ensures(Contract.Result<object>() is string); return s; } }
      
      







この例では、相続人メソッドの必要量が少なくなりました。空の文字列が正しい値になりました。 より正確な結果が得られます。オブジェクトが返されるだけでなく、 文字列返されます (ただし、コンパイラではなく静的チェッカーによって保証されます)。





事後条件強化の典型的な例は、派生クラスがより具体的な型を返す機能です。 この機能は戻り値型共分散と呼ばれ、C ++やJavaなどの言語で使用できます。 C#がこの機能をサポートしている場合、Derived.Fooメソッドの署名を変更して、 オブジェクトではなく文字列を返すことができます 前提条件を弱め、後条件を強化するもう1つの例は、C#言語の4番目のバージョンから利用可能なデリゲートとインターフェイスの共分散と反分散です。 契約による設計 の記事で、条件の「厳格さ」について詳しく読んでください ソフトウェアの正確性 、および契約と継承について- 契約による設計 の記事 継承



Code Contracts開発者は、事後条件を無意味に弱める可能性があると考えたため、そのような機会はありません。 上記のDerivedクラスコードはコンパイルされますが、前提条件はDerived method です。 Fooは弱まりません。つまり、空の文字列を渡すと、前提条件に違反します。 ただし、事前条件とは異なり、事後条件ではほぼすべてのものが順番に並んでいます。 事後条件(ところで、クラスの不変条件のような)は「合計」されます。これにより、より多くのことを保証できます。 ( Derived。Fooメソッドの本体を変更して、場合によっては文字列ではなくintを返す場合、この違反は静的チェッカーによって検出され、実行時にチェックされます。)



事後条件とインターフェース




それでは、基本クラスからインターフェースに移りましょう。 最初のセクションでは、コレクションの要素数を「削減しない」事後条件であるICollectionインターフェイスを調べました。 この事後条件は、BCLのTCollectionのICollection コントラクトに基づいて取得されます。 コードコントラクトをインストールした後、独自のクラスのコントラクトを作成できるだけでなく、BCLの標準クラスコントラクトを使用することもできます。



しかし、標準のインターフェイスコントラクトの分析に進む前に、独自のインターフェイスの階層を作成して、 Addメソッドの事後条件を試してみましょう。



 [ContractClass(typeof(ListContract))] public interface IList : ICollection { } [ContractClassFor(typeof(IList))] internal abstract class ListContract : IList { public void Add(string s) { // Lets create stronger postcondition than ICollection.Add Contract.Ensures(Count == Contract.OldValue(Count) + 1); } //   Count   Contains   }
      
      







実際のIListインターフェースは、事後条件だけでなく、他の多くの興味深いものも追加しますが、この場合は重要ではありません。 次に、インターフェイスの事後条件に違反するIListインターフェイスを実装するクラスを追加します。



 public class CustomList : IList { private readonly List<string> _backingList = new List<string>(); public void Add(string s) { // IList postcondition is Count = OldCount + 1, // we're violating it _backingList.Add(s); _backingList.Add(s); } public int Count { get { return _backingList.Count; } } public bool Contains(string s) { return _backingList.Contains(s); } }
      
      







IListインターフェースのAddメソッドの事後条件に明らかに違反します。要素の数が1つではなく、すぐに2増えるためです。しかし、悲しいことは、静的アナライザーもリライタもインターフェースの事後条件の強化にまったく反応しないことです。 実際、この機能はコードコントラクトライブラリではサポートされていません(さらに、開発者はこれをバグではなく機能と見なしています。詳細はこちら )。 そのため、現時点では、仮想メソッドの事後条件を強化できます。何らかのインターフェイスを実装するクラスでは事後条件を強化できますが、相続人のインターフェイスでは事後条件を強化できません



この点の不愉快さは次のとおりです:まず、インターフェースのより厳密な事後条件の存在を見つける唯一の方法は、契約コードを手動で検索することです(静的チェッカーもリライターも相続事後条件に関する情報を結果コードに追加しないことを思い出します); 第二に、この例は人為的ではなく、標準のBCLコレクションインターフェイスを使用しているときにこの問題が発生する可能性があります。



TのICollectionとTの IListの 契約




mscorlibのアセンブリを注意深く掘り下げた場合 契約 コードコントラクトをインストールした後に表示されるdllを使用すると、特に.NET Frameworkの標準クラスのコントラクトとコレクションのコントラクトについて多くの興味深いことがわかります。 TのメインICollection および Tの IListインターフェイスメソッドのコントラクトは次のとおりです



 // From mscorlib.Contracts.dll [ContractClassFor(typeof(ICollection<>))] internal abstract class ICollectionContract<T> : ICollection<T>, IEnumerable<T>, IEnumerable { public void Add([MarshalAs(UnmanagedType.Error)] T item) { Contract.Ensures(this.Count >= Contract.OldValue<int>(this.Count), "this.Count >= Contract.OldValue(this.Count)"); } public void Clear() { Contract.Ensures(this.Count == 0, "this.Count == 0"); } public int Count { get { int num = 0; Contract.Ensures(Contract.Result<int>() >= 0, "Contract.Result<int>() >= 0"); return num; } } } // From mscorlib.Contracts.dll [ContractClassFor(typeof (IList<>))] internal abstract class IListContract<T> : IList<T>, ICollection<T>, IEnumerable<T>, IEnumerable { void ICollection<T>.Add([MarshalAs(UnmanagedType.Error)] T item) { Contract.Ensures(this.Count == (Contract.OldValue<int>(this.Count) + 1), "Count == Contract.OldValue(Count) + 1"); } public int Count { get { int num = 0; return num; } } }
      
      









.NET Frameworkのさまざまなバージョンの契約はさまざまな場所にあります。たとえば、フレームワークの4番目のバージョンの契約は、次のパスにあります。「%PROGRAMS%\ Microsoft \ Contracts \ Contracts \ .NETFramework \ v.4.0 \」。 契約のあるアセンブリには、OriginalAssemblyName.Contracts.dll:mscorlib.Contracts.dll、System.Contracts.dll、System.Xml.Contracts.dllという名前が付けられます。



ご覧のとおり、リストの事後条件は非常に強力であり、 Addメソッドを呼び出すと、リストに新しい要素が1つだけ表示される必要があります。 2つのインターフェイスの事後条件の違いは、 Addメソッドを呼び出すときにすべてのBCLコレクションが新しい要素を追加するわけではないという事実によるものです( HashSetSortedSetが既にコレクションにある場合、要素を追加しません)。 ただし、すべてのリストに追加される新しいアイテムは1つだけです。 この問題は、明示的な事後条件を特定のコレクションクラス( Tの リスト またはこの例ではDoubleListクラス)に追加することで解決されますが、この場合、インターフェイスコントラクトの主な機能は失われます:クラスファミリの動作を指定する機能。



おわりに




.NETでは期待される動作に関する情報が含まれていないため、すべての開発者がインターフェイスコントラクトまたは抽象メソッドについて快適に考えるわけではありません。 しかし、一方でそれを見ると、そのような方法の契約の重要性ははるかに高くなっています。 特定のメソッドの場合、その実装を見て、明示的または暗黙的な前提条件と事後条件を決定できます。 しかし、インターフェイスの契約を決定するには、不十分な非公式の文書から進むか、このインターフェイスのすべての実装を分析して、置換原則に従って違反されるべきではないその動作の「共通」分母を決定します。



サイトリンク


  1. GitHubのContractsAndInheritanceプロジェクト 。 この記事のすべての例とテストおよびコメントが含まれています。

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

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



All Articles