インターフェイスの実装を強制して特定の方法で動作させる方法


8

次のインターフェースがあるとします

public interface IUserRepository
{
    User GetByID(int userID);
}

ユーザーが見つからない場合、このインターフェイスの実装者に例外をスローさせるにはどうすればよいですか?

コードだけで行うことは不可能だと思うので、意図した動作を実装するように実装者にどのように強制しますか?コード、ドキュメントなどを通じてですか?

この例では、具体的な実装により、 UserNotFoundException

public class SomeClass
{
    private readonly IUserRepository _userRepository;

    public SomeClass(IUserRepository userRepository)
    {
        _userRepository = userRepository;
    }

    public void DisplayUser()
    {
        try 
        {
            var user = _userRepository.GetByID(5);
            MessageBox.Show("Hello " + user.Name);
        }
        catch (UserNotFoundException)
        {
            MessageBox.Show("User not found");
        }
    }
}

3
これは非常に興味深い質問です。正解には、属性やILハックが関係しているのではないかと思います。一般に、各実装者は独自のタイプの例外をスローできるはずなので、それを試みることは間違った選択であると思いますが、それでもまだ有用であると考えられます。たぶん、あなたはテンプレートメソッドで抽象クラスを使うだけでしょうか?
Magus

15
テクノロジーは人々の問題を解決しません。インターフェースに違反した場合は、止まるまで棒で叩いてください。
Telastyn、2014年

1
@Telastynたぶん代わりに人々を棒で打つプログラムを作るべきだ。
マシュー

2
@Doval:それで、インターフェースは、有効なユーザーが存在するかどうかを、実装がそれなしにサポートしていないかどうかをどうやって知るのでしょうか?
Robert Harvey

1
@matthew-次に、プログラムは「停止するまで」を適切に推測する必要があります。これにより、問題全体が停止します(スティックを振るうプログラムで、ユーザーがインターフェイスに違反しているかどうかを確認できる場合は、インターフェイスは必要ありません)。 。
Telastyn 2014年

回答:


9

これは、C#から意図的に省略された言語機能です。簡単に言うと、IUserRepository.GetByIDユーザーが見つからないこと以外の理由でが完全に失敗する可能性があるため、そのようなことが発生しない場合に特定のエラーを要求する必要はありません。何らかの理由でこの動作を強制したい場合は、2つのオプションがあります。

  1. User不適切に初期化された場合にクラス自体が例外をスローするようにクラスを定義します。
  2. IUserRepositoryこの動作を明示的にテストするための単体テストを記述します。

これらのオプションはどちらも「ドキュメントに含めない」ことに注意してください。理想的には、とにかくそれを行う必要があります。特に、特定のエラータイプを強制する理由を説明できるドキュメントがあるからです。


2
チェック例外があっても、適切なタイミングで適切な例外をスローするように実装者に強制することはできません。
2014年

7

メソッドが例外をスローする可能性があることを宣言できるJavaのような言語であっても、インターフェースを介して例外をスローするように実装に要求する方法はありません。

例外がスローされることを確実にする方法があります(絶対ではありませんがある程度)。インターフェースの抽象的な実装を作成できます。次に、GetUserメソッドを抽象クラスのfinalとして実装し、戦略パターンを使用して、サブクラスの別の保護されたメンバーを呼び出し、有効なユーザー(nullなど)以外のものを返す場合は例外をスローします。これは、たとえば、他の開発者がnullオブジェクト型を返した場合でも落ちる可能性がありますUserが、実際にここで意図を覆すために作業する必要があります。また、単にインターフェースを再実装することもできますが、これも悪いので、インターフェースを完全に抽象クラスに置き換えることを検討してください。

(同様の結果は、ラッピングデコレーターのようなものでサブクラス化する代わりに、委譲を使用して達成できます。)

別のオプションは、すべての実装コードが含まれるために渡す必要がある適合性テストスイートを作成することです。これがどの程度効果的であるかは、他のコードとのリンクをどの程度制御できるかに依存します。

また、このような要件が想定されているが、コードに完全に適用することができない場合は、明確なドキュメントとコミュニケーションが与えられることにも同意します。


コード例:

サブクラスメソッド:

public abstract class ExceptionalUserRepository : IUserRepository
{
    public sealed User GetUser(int user_id)
    {
        User u = FindUserByID(user_id);
        if(u == null)
        {
            throw new UserNotFoundException();
        }
        return u;
    }

    // subclasses implement this method instead
    protected abstract User FindUserByID(int user_id);
    // More code here
}

デコレータメソッド:

public sealed class DecoratedUserRepository : IUserRepository
{
    private readonly IUserRepository _userRepository;

    public DecoratedUserRepository(IUserRepository userRepository)
    {
        _userRepository = userRepository;
    }

    public User GetUser(int user_id)
    {
        User u = _userRepository.GetUser(user_id);
        if(u == null)
        {
            throw new UserNotFoundException();
        }
        return u;
    }

    // More code here
}

public class SomeClass
{
    private readonly IUserRepository _userRepository;

    // They now *have* to pass in exactly what you want
    public SomeClass(DecoratedUserRepository userRepository)
    {
        _userRepository = userRepository;
    }
    // More code
}

私が以前忘れていたことにしたい最後の要点は、これらのいずれかを実行すると、より具体的な実装に縛られることになります。つまり、実装開発者は、それほど自由度が低くなります。


3

例外をスローする振る舞いを実装したい開発者と一緒に作業する場合、存在しないユーザーを取得しようとした場合に例外がスローされるかどうかを確認するユニットテストを(それらに対して)作成することができます。

したがって、テストするメソッドを知る必要があります。私はC#に精通していませんが、リフレクションを使用してIUserRepositoryを実装するすべてのクラスを検索し、それらを自動的にテストして、開発者が別のクラスを追加した場合でも、テストの実行時にテストされるようにすることができます。

しかし、それは、開発者が実装を間違って実行することに苦しんでいる場合に実際に実行できることだけです。理論的には、インターフェイスはビジネスロジックの実装方法を定義するためのものではありません。

これは本当に興味深い質問なので、他の回答を楽しみにしています!


3

できません。それがインターフェースに関するものです-彼らはいつでも誰でもそれらを実装することができます。インターフェースには潜在的な実装が無数にあり、それらを強制的に正しいものにすることはできません。インターフェイスを選択すると、特定の実装をシステム全体で使用することを強制する権利がなくなります。一体、あなたが持っているのはインターフェースでもなく、それは関数です。関数を渡すコードを書いています。

この方法を使用する必要がある場合は、実装が準拠しなければならない仕様を明確に文書化し、誰も故意にその仕様に違反しないことを信頼する必要があります。

代わりに、Java / C#に似た言語のクラスに変換される抽象データ型を使用することもできます。問題は、主流の言語ではADTのサポートが弱く、ファイルシステムのtomfooleryがなければクラスの実装を切り替えることができないことです。しかし、それでも構わないのであれば、システムに実装が1つしかないことを確認できます。これはインターフェースからのステップアップであり、コードはいつでも壊れる可能性があり、いつ壊れるかは、インターフェースのどの実装がコードを壊し、どこから来たかを突き止める必要があります。

編集:ILハッキングでさえ、実装の特定の側面の正確さを保証することはできないことに注意してください。たとえばGetByID、値を返すか、例外をスローする必要があることが暗黙的に想定されています。誰かが単純に無限ループに入り、どちらも実行しない実装を書くことができます。ランタイム中にコードが永久にループすることを証明することはできません-これをうまくやれば、停止問題は解決しました。些細なケース(たとえばを認識するwhile (true) {})は検出できるかもしれませんが、任意のコードは検出できません。


まあ、インターフェースはいくつかの間違った実装を防ぎます:型が間違っているもの(インターフェースのメソッドシグネチャと一致しないもの)。OPが望む制約を表現することはできませんが、それを表現できる型システムはほとんどありません。

@delnanコードの一部に異なるタイプがある場合、それを実装と呼ぶことはほとんどできません!しかし、私たちは意味論について議論しています。私はあなたの意味を理解します。
ドバル2014年

もう1つ:「抽象データ型」は、ドキュメントによく似た非公式な概念です。マシンチェック可能にするには、型システムを構築し、その型システムの同等のインターフェイスを使用する必要があります。

@delnanそれは誤解です。「データの抽象化の理解について」を参照してください。はい、ADTはインターフェースのようにデータを抽象化します。しかし、ADTは、既存の型のIDを不明瞭にすることでそのようにします。これは、メインストリームOOPのプライベートフィールドを持つクラスに相当します。隠蔽タイプは、フィールドを非公開にしたレコードです。一方、インターフェースは関数のバンドルであり、実装タイプはシグネチャのどこにも現れません。これにより、複数の実装と代替性が可能になります。
Doval 2014年

これは、私が慣れている「抽象データ型」の使い方とはかなり違うようです。私や他の多くの人が抽象データ型として知っていることは、言語にとらわれない方法でデータ型について話すためのツールにすぎません。あなたが説明しているのは、前述の抽象データ型の概念を解釈および実装する型システム機能(のクラス)のようです。

2

希望する動作を100%強制する方法はありませんが、コードコントラクトを使用すると、実装がnullを返してはならず、UserオブジェクトにIDが渡されていると言うことができます(したがって、1が渡された場合、userid 0のオブジェクトは失敗します)。その上、実装に期待することを文書化することも役立ちます。


0

これはおそらくこの質問に答えるのが非常に遅いですが、これについての私の考えを共有したいと思います。すべての回答が示唆するように、インターフェイスで制約を強制することはできません。ハックを使っても難しいでしょう。

これを実現する1つの方法は、おそらく.NETに固有ですが、以下のようなインターフェースを設計することです-

public interface IUserRepository
{
    Task<User> GetByID(int userId);
}

タスクは特定のタイプの例外をスローすることを強制しませんが、このインターフェースのすべての実装者の意図を伝えるための準備があります。.NETのタスクには、結果にアクセスするためのTask.Result、例外がある場合はTask.Exceptionなど、特定の使用パターンがあります。さらに、これは非同期に使用することもできます。


0

他の多くの人がすでに回答しているように、インターフェースでそれを行うことはできません。
しかし、抽象基本クラスでそれを行うことができます:

public abstract class UserRepositoryBase
{
  public User GetByID (int userID)  // not virtual because you don't want to allow overriding
  {
    User user = null;
    try
    {
      user = GetByID_EXEC (userID);
    }
    catch (UserNotFoundException)
    {
      throw;  // rethrow exception because it's the correct type.
    }
    catch (Exception)
    {
      // do whatever you want
    }
    if (user != null)
      return user;
    throw new UserNotFoundException ();
  }

  protected abstract User GetByID_EXEC (int userID);
}
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.