web-dev-qa-db-ja.com

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

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

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");
        }
    }
}
8
Matthew

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

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

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

9
DougM

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

例外がスローされることを(ある程度ではあるが絶対ではないが)保証する方法があるかもしれません。インターフェースの抽象的な実装を作成できます。次に、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
}

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

7
cbojar

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

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

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

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

3
Doval

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

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

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

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

3
valenterry

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

2
Andy

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

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);
}
0
Tobias Knauss

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

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

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

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

0
Bhalchandra K