次のように、インターフェースSomeMethod
にメンバーISomeInterface
があるとします。
public interface ISomeInterface
{
int SomeMethod(string a);
}
私のプログラムでは、ISomeInterface
のすべてのコンシューマーが、返されるintが5より大きいことを前提に行動します。
これを解決するには3つの方法が思い浮かびます-
1)ISomeInterface
を消費するすべてのオブジェクトについて、返されたint> 5であることを表明します。
2)ISomeInterface
を実装するすべてのオブジェクトについて、返されるintが> 5であると主張します。
上記の2つのソリューションはいずれも、開発者がISomeInterface
を実装または使用するたびにこれを行うことを覚えておく必要があるため、面倒です。さらに、これは良くないインターフェースの実装に依存しています。
3)実際にこれを行うと考えることができる唯一の方法は、ISomeInterface
も実装し、以下のように基礎となる実装を返すラッパーを用意することです。
public class SomeWrapper : ISomeInterface
{
private ISomeInterface obj;
SomeWrapper(ISomeInterface obj)
{
this.obj = obj;
}
public int SomeMethod(string a)
{
int ret = obj.SomeMethod("hello!");
if (!(ret > 5))
throw new Exception("ret <= 5");
else
return ret;
}
}
ただし、ここでの問題は、ISomeInterface
クラスの機能を介してSomeWrapper
の実装の詳細に依存していることですが、1つの場所に限定されているという利点があります。
これは、インターフェイスが期待どおりに実装されていることを確認する最良の方法ですか、それともより良い代替手段がありますか?私はインターフェースがこれのために設計されていないかもしれないことを理解していますが、すべてのアサーションを行う必要なしにインターフェースのメンバーシグネチャ内で伝えることができるものよりも特定の方法で動作すると仮定して、いくつかのオブジェクトを使用するためのベストプラクティスは何ですか?それがインスタンス化される時ですか?実装することになっている追加の事柄や制限を指定することさえできれば、インターフェースは良いコンセプトのように思えます。
int
を返す代わりに、検証がハードコードされた値オブジェクトを返します。これは原始的な執着とその修正の事例です。
// should be class, not struct as struct can be created without calling a constructor
public class ValidNumber
{
public int Number { get; }
public ValidNumber(int number)
{
if (number <= 5)
throw new ArgumentOutOfRangeException("Number must be greater than 5.")
Number = number;
}
}
public class Implementation : ISomeInterface
{
public ValidNumber SomeMethod(string a)
{
return new ValidNumber(int.Parse(a));
}
}
この方法では、実装内で検証エラーが発生するため、開発者がこの実装をテストすると、エラーが表示されます。メソッドが特定のオブジェクトを返すようにすると、プレーンな値を返すだけでは不十分な場合があります。
otheranswers を補足するために、幅広いコンテキストを提供することにより、OPの以下のメモに部分的にコメントしたいと思います。
実装することになっている追加の事柄や制限を指定することさえできれば、インターフェースは良いコンセプトのように思えます。
あなたはここで良い点を作っています!このような制限(制約)を指定できるレベルを検討してみましょう。
以下のすべての項目について詳しく説明します。その前に、1から4に移行するにつれて、制約がますます弱くなると言います。たとえば、ポイント4のみに依存している場合、開発者がドキュメントを正しく適用していることに依存しており、誰もがそうすることはできません。それらの制約が人間以外で満たされているかどうかを伝えることができます。もちろん、これは人間の性質上、バグを含む可能性がはるかに高くなります。
したがって、常にポイント1で制約のモデリングを開始する必要があり、それが(部分的に)不可能である場合にのみ、ポイント2を試す必要があります。理論的には、常に言語の型システムに依存する必要があります。ただし、これを可能にするには、型チェックの速度と労力、および開発者が型を理解できるという点で、非常に強力な型システムが必要です。後者については Scala 2.8コレクションライブラリは「歴史上最も長い自殺メモ」のケースですか? を参照してください。
C#などのほとんどの型付き(OO風味)言語では、次のインターフェイスを簡単に使用できます。
_public interface ISomeInterface
{
int SomeMethod(string a);
}
_
ここでは、型システムを使用して、int
などの型を指定できます。次に、コンパイラーのタイプチェッカーコンポーネントは、コンパイル時に、インプリメンターが常にSomeMethod
から整数値を返すことを保証します。
多くのアプリケーションは、JavaおよびC#にある通常の型システムで既に構築できます。ただし、念頭に置いていた制約について、つまり戻り値が5より大きい整数であるということは、これらの型システムは弱すぎます。実際、一部の言語はより強力な型システムを備えており、int
の代わりに_{x: int | x > 5}
_を記述できます1、つまり5より大きいすべての整数の型。これらの言語の一部では、実装者として常に5より大きい値を返すことも証明する必要があります。これらの証明は、コンパイル時にコンパイラによっても検証されます。
C#には一部の型が含まれていないため、ポイント2と3を使用する必要があります。
この他の答え は、言語内のメタアノテーションの例をすでに提供しています。これは、Java here:
_@NotNull
@Size(min = 1)
public List<@NotNull Customer> getAllCustomers() {
return null;
}
_
静的分析ツールは、メタアノテーションで指定されたこれらの制約がコードで満たされているかどうかを検証しようとします。検証できない場合、これらのツールはエラーを報告します。2 通常、コンパイル時に従来のコンパイラーと一緒に静的分析ツールを使用します。つまり、ここでもコンパイル時に制約チェックが行われます。
別のアプローチは、言語の外部にあるメタ注釈を使用することです。たとえば、Cでコードベースを作成し、そのCコードベースを参照して、まったく異なる言語でいくつかの制約が満たされていることを証明できます。 「Cコードの検証」、「CコードCoqの検証」などのキーワードで例を見つけることができます。
このレベルの制約チェックでは、コンパイルと静的解析の時間からruntimeにチェックを完全に外部委託します。実行時に戻り値が制約を満たすかどうか(たとえば、5より大きい)を確認し、満たさない場合は例外をスローします。
Otheranswers は、これがコード的にどのように見えるかをすでに示しました。
ただし、このレベルでは柔軟性が高くなりますが、制約のチェックをコンパイル時から実行時まで延期することになります。これは、バグがソフトウェアの顧客でおそらく遅くなる可能性があることを意味します。
ランタイムアサーションは非常に柔軟ですが、それでも、考えられるすべての制約をモデル化できるわけではありません。戻り値に制約を設定するのは簡単ですが、たとえば、コードのある種の「監視」ビューが必要になるため、コードコンポーネント間の相互作用をモデル化するのは難しい(読み取り:扱いにくい)場合があります。
たとえば、メソッドint f(void)
は、その戻り値がゲーム内のプレーヤーの現在のスコアであることを保証しますが、int g(void)
が呼び出されていない限り、 f
。この制約は、おそらく人間指向のドキュメントを順守する必要があるものです。
1:キーワードは「依存型」、「精製型」、「液体型」。主な例は、定理証明者のための言語です。 Coq証明アシスタント で使用されるGallina。
2:制約で許可する表現の種類に応じて、履行チェックは 決定不能な問題 になる可能性があります。実際には、これはプログラムされたメソッドが指定された制約を満たしているが、静的分析ツールはそれらを証明できないことを意味します。あるいは、別の言い方をすると、エラーに関して偽陰性があるかもしれません。 (ただし、ツールにバグがなければ、誤検知は発生しません。)
コントラクトで設計しようとしているが、そのコントラクトでは、戻り値が5より大きくなければならない。残念ながら、インターフェースに依存している場合、唯一のコントラクトはメソッドシグネチャです。
代わりに抽象クラスを使用することをお勧めします。ここに私がJavaで取るアプローチがあります:
public abstract class SomeAbstraction {
public final int someMethod(String a) {
// You may want to throw an exception instead
return Math.max(6, someAbstractMethod());
}
protected abstract int someAbstractMethod();
}
サブクラスが別のパッケージに分離されている限り(someAbstractMethod
にアクセスできない場合)、この抽象化のクライアントはsomeMethod
にのみアクセスでき、常に戻り値に安全に依存できます。契約は1か所で実施され、すべてのサブクラスはそれを知っているかどうかにかかわらず従わなければなりません。 final
でsomeMethod
キーワードを使用すると、サブクラスがそのコントラクトを上書きすることによってそのコントラクトを強制的に破ることを防ぐという追加の利点があります。
注:通常、契約違反は例外の動作である必要があります。したがって、戻り値を強制するだけでなく、例外をスローしたり、エラーをログに記録したりすることをお勧めします。しかし、これはユースケースに完全に依存します。
許容可能な戻り値を制限する検証アノテーションがある場合があります。以下は、Java baeldung.com から取得したSpringの例)の例ですが、C#には 類似の機能 があります。
@NotNull
@Size(min = 1)
public List<@NotNull Customer> getAllCustomers() {
return null;
}
このアプローチを使用する場合、次のことを考慮する必要があります。
検証がビジネスモデルの一部である場合、このアプローチはお勧めしません。この場合、Euphoricの answer の方が適しています。返されるオブジェクトは、豊富な ドメインモデル の作成に役立つ 値オブジェクト になります。このオブジェクトには、行うビジネスの種類に応じた制限のある意味のある名前を付ける必要があります。たとえば、ここでは、日付がユーザーにとって妥当であることを検証できます。
public class YearOfBirth
private final year;
public YearOfBirth(int year){
this.year = year;
if(year < 1880){
throw new IllegalArgumentException("Are you a vampire or what?");
}
if( year > 2020){
throw new IllegalArgumentException("No time travelers allowed");
}
}
}
良い点は、この種のオブジェクトが、シンプルでテスト可能なロジックを持つ非常に小さなメソッドを引き付けることができるということです。例えば:
public String getGeneration(){
if( year < 1964){
return "Baby Boomers";
}
if( year < 1980 ){
return "Generation X";
}
if( year < 1996){
return "Millenials";
}
// Etc...
}
複雑にする必要はありません。
public interface ISomeInterface
{
// Returns the amount by which the desired value *exceeds* 5.
uint SomeMethodLess5(string a);
}
インターフェースのすべての実装を見つけるための単体テストを作成し、それらのそれぞれに対してテストを実行できます。
var iType= typeof(ISomeInterface);
var types = AppDomain.CurrentDomain.GetAssemblies()
.SelectMany(s => s.GetTypes())
.Where(p => iType.IsAssignableFrom(p));
foreach(var t in types)
{
var result = t.SomeMethod("four");
Assert.IsTrue(result > 4, "SomeMethod implementation result was less than 5");
}
ISomeInterfaceの新しい実装がプロジェクトに追加されると、このテストはそれをテストでき、5未満の値が返された場合は失敗するはずです。もちろん、これは、このメソッドへの入力からのみ適切にテストできることを前提としています。ここでは「4」を使用しています。この呼び出しを設定するには、他の方法が必要になる場合があります。
ほとんどの言語には、Intellisenseコードドキュメントの形式があります。 C#の場合、その情報を見つけることができます ここ 。簡単に言えば、あなたがコメントするドキュメントですIDE Intellisenseは解析でき、ユーザーがそれを使用したいときにユーザーが利用できるようにします。
インターフェースのドキュメントが言うは、呼び出しの動作ですが、それがどのように動作するかについての唯一の実際の規約です。結局のところ、インターフェイスは、優れた出力を提供する乱数ジェネレータと、常に4を返すもの(完全にランダムなサイコロによって決定される)との違いを知る方法を知りません。
ドキュメンテーションの後、インターフェイスを実装するクラスのインスタンスが与えられ、さまざまなユースケースを実行したときに予想される動作を提供する、インターフェイスの単体テストスイートが必要です。あなたがcould動作を強化するために単体テストを抽象クラスに焼き付けている間、それはやり過ぎであり、おそらくそれ以上の苦痛を引き起こします。
いくつかの言語が何らかの形式のメタ注釈をサポートしていることにも言及する必要があります。コンパイル時に評価される効果的なアサーション。実行できるチェックの種類は限られていますが、少なくともコンパイル時に簡単なプログラミングの間違いを確認できます。これは、インターフェイスのエンフォーサーよりも、コンパイラーがコード文書を支援するものと見なされるべきです。