So erzwingen Sie die Implementierung der Schnittstelle, um sich auf eine bestimmte Weise zu verhalten

8

Angenommen, Sie hätten die folgende Schnittstelle

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

Wie würden Sie Implementierer dieser Schnittstelle dazu zwingen, eine Ausnahme auszulösen, wenn ein Benutzer nicht gefunden wird?

Ich vermute, dass es nicht möglich ist, nur mit Code zu arbeiten. Wie würden Sie Implementierer dazu zwingen, das beabsichtigte Verhalten zu implementieren? Sei es durch Code, Dokumentation usw.?

In diesem Beispiel wird erwartet, dass die konkrete Implementierung a auslöst 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");
        }
    }
}
Matthew
quelle
3
Das ist wirklich eine sehr interessante Frage. Ich habe das Gefühl, dass die richtige Antwort Attribute oder einen IL-Hack beinhalten könnte. Im Allgemeinen würde ich davon ausgehen, dass der Versuch eine falsche Wahl ist, da jeder Implementierer in der Lage sein sollte, seine eigenen Arten von Ausnahmen auszulösen, aber es ist dennoch denkbar nützlich. Vielleicht würden Sie nur eine abstrakte Klasse mit einer Vorlagenmethode verwenden?
Magus
15
Technologie löst keine Menschenprobleme. Wenn Personen die Benutzeroberfläche verletzen, schlagen Sie sie mit einem Stock, bis sie anhalten.
Telastyn
1
@Telastyn Vielleicht sollte ich stattdessen ein Programm erstellen, das Leute mit Stöcken schlägt.
Matthew
2
@Doval: Woher weiß die Schnittstelle, ob ein gültiger Benutzer vorhanden ist oder nicht, ohne dass eine Implementierung dies unterstützt?
Robert Harvey
1
@matthew - dann müsste Ihr Programm "bis sie aufhören" richtig schließen, was zu dem ganzen Problem mit dem Anhalten zurückführt (wenn Ihr Stick-Wielding-Programm erkennen könnte, dass Leute seine Schnittstelle verletzen oder nicht, würden Sie es nicht brauchen) .
Telastyn

Antworten:

9

Dies ist eine Sprachfunktion, die in C # absichtlich weggelassen wurde . Ganz einfach, es ist durchaus möglich, dass ein IUserRepository.GetByIDFehler aus einem anderen Grund als dem nicht gefundenen Benutzer fehlschlägt. Sie möchten also keinen bestimmten Fehler benötigen, wenn dies nicht möglich ist. Sie haben zwei Möglichkeiten, wenn Sie dieses Verhalten aus irgendeinem Grund erzwingen möchten:

  1. Definieren Sie die UserKlasse so, dass sie selbst die Ausnahme auslöst, wenn sie nicht ordnungsgemäß initialisiert wurde.
  2. Schreiben Sie Unit-Tests für IUserRepositorydiesen expliziten Test für dieses Verhalten.

Beachten Sie, dass keine dieser Optionen "in die Dokumentation aufnehmen" ist. Im Idealfall sollten Sie dies trotzdem tun, insbesondere in der Dokumentation können Sie erklären, warum Sie einen bestimmten Fehlertyp erzwingen möchten.

DougM
quelle
2
Selbst mit aktivierten Ausnahmen können Sie Implementierer nicht zwingen , die richtige Ausnahme zur richtigen Zeit auszulösen.
Svick
7

Es gibt keine Möglichkeit, eine Implementierung zum Auslösen einer Ausnahme über eine Schnittstelle zu verlangen , selbst in Sprachen wie Java, in denen Sie deklarieren können, dass eine Methode eine Ausnahme auslösen könnte.

Es kann eine Möglichkeit geben, sicherzustellen (bis zu einem gewissen Grad, aber nicht absolut), dass eine Ausnahme ausgelöst wird. Sie können eine abstrakte Implementierung Ihrer Schnittstelle erstellen. Sie können die GetUserMethode dann als final in der abstrakten Klasse implementieren und mithilfe des Strategiemusters ein anderes, geschütztes Mitglied der Unterklasse aufrufen und eine Ausnahme auslösen, wenn etwas anderes als ein gültiger Benutzer zurückgegeben wird (z. B. null). Dies kann immer noch zum Erliegen kommen, wenn der andere Entwickler beispielsweise einen Nullobjekttyp zurückgibt User, aber er müsste wirklich arbeiten, um die Absicht hier zu untergraben. Sie könnten auch nur Ihre Schnittstelle neu implementieren, auch schlecht, so dass Sie in Betracht ziehen könnten, die Schnittstelle vollständig durch die abstrakte Klasse zu ersetzen.

(Ähnliche Ergebnisse können mit der Delegierung erzielt werden, anstatt mit einem Verpackungsdekorateur eine Unterklasse zu erstellen.)

Eine andere Option könnte darin bestehen, eine Konformitätstestsuite zu erstellen, die der gesamte implementierende Code übergeben muss, um aufgenommen zu werden. Wie effektiv dies ist, hängt davon ab, wie viel Kontrolle Sie über die Verknüpfung des anderen Codes mit Ihrem Code haben.

Ich stimme auch anderen zu, dass eine klare Dokumentation und Kommunikation selbstverständlich sind, wenn eine solche Anforderung erwartet wird, aber im Code nicht vollständig durchgesetzt werden kann.


Codebeispiele:

Unterklassenmethode:

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
}

Dekorationsmethode:

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
}

Ein letzter kurzer Punkt, den ich vergessen möchte, ist, dass Sie sich durch eine dieser Aktionen an eine spezifischere Implementierung binden, was bedeutet, dass Implementierungsentwickler so viel weniger Freiheit erhalten.

cbojar
quelle
3

Wenn Sie mit den Entwicklern zusammenarbeiten, die das Verhalten zum Auslösen von Ausnahmen implementieren möchten, können Sie möglicherweise Unittests (für sie) schreiben, um zu überprüfen, ob eine Ausnahme ausgelöst wird, wenn Sie versuchen, einen Benutzer zu finden, der nicht vorhanden ist.

Sie müssen daher wissen, welche Methoden getestet werden sollen. Ich bin mit C # nicht vertraut, aber Sie könnten Reflection verwenden, um nach allen Klassen zu suchen, die das IUserRepository implementieren, und diese automatisch zu testen. Selbst wenn ein Entwickler eine weitere Klasse hinzufügt, wird diese beim Ausführen der Tests getestet.

Dies ist jedoch nur dann möglich, wenn Sie wirklich darunter leiden, dass Entwickler die Implementierungen falsch ausführen, mit denen Sie arbeiten müssen. Theoretisch dienen Schnittstellen nicht dazu zu definieren, wie die Geschäftslogik implementiert werden muss.

Ich freue mich auf weitere Antworten, da dies wirklich eine interessante Frage ist!

Valenterry
quelle
3

Das kannst du nicht. Das ist die Sache mit Schnittstellen - sie ermöglichen es jedem, sie jederzeit zu implementieren. Es gibt unendlich viele mögliche Implementierungen für Ihre Schnittstelle, und Sie können keine davon erzwingen, korrekt zu sein. Durch die Auswahl einer Schnittstelle haben Sie Ihr Recht auf Durchsetzung der Verwendung einer bestimmten Implementierung im gesamten System verwirkt. Was Sie haben, ist nicht einmal eine Schnittstelle, sondern eine Funktion. Sie schreiben Code, der Funktionen weitergibt.

Wenn Sie diesen Weg gehen müssen, müssen Sie lediglich die Spezifikation, der die Implementierungen entsprechen müssen, klar dokumentieren und darauf vertrauen, dass niemand diese Spezifikation absichtlich verletzt.

Die Alternative besteht darin, einen abstrakten Datentyp zu haben, der in eine Klasse in Java / C # -ähnlichen Sprachen übersetzt wird. Das Problem ist, dass Mainstream-Sprachen ADTs nur schwach unterstützen und Sie die Implementierung der Klasse ohne Dateisystem-Tomfoolery nicht ändern können. Wenn Sie jedoch bereit sind, damit zu leben, können Sie sicherstellen, dass nur eine Implementierung im System vorhanden ist. Dies ist ein Fortschritt gegenüber Schnittstellen, bei denen Ihr Code jederzeit beschädigt werden kann, und wenn dies der Fall ist, müssen Sie herausfinden, welche Implementierung der Schnittstelle Ihren Code beschädigt hat und woher er stammt.

BEARBEITEN : Beachten Sie, dass Sie selbst mit IL-Hacks nicht die Richtigkeit bestimmter Aspekte einer Implementierung sicherstellen können. Beispielsweise wird implizit angenommen, dass GetByIDentweder ein Wert zurückgegeben oder eine Ausnahme ausgelöst werden muss. Jemand könnte eine Implementierung schreiben, die einfach in eine Endlosschleife geht und beides nicht tut. Sie können zur Laufzeit nicht beweisen, dass sich der Code für immer wiederholt. Wenn Sie dies schaffen, haben Sie das Halteproblem gelöst. Möglicherweise können Sie triviale Fälle erkennen (z. B. a erkennen while (true) {}), aber keinen beliebigen Code.

Doval
quelle
Nun, eine Schnittstelle verhindert einige falsche Implementierungen: Diejenigen, die schlecht typisiert sind (stimmen nicht mit den Methodensignaturen in der Schnittstelle überein). Es kann die von OP gewünschte Einschränkung nicht ausdrücken, aber nur wenige Typsysteme können dies.
@delnan Wenn ein Code einen anderen Typ hat, kann man ihn kaum als Implementierung bezeichnen! Aber wir streiten uns über Semantik. Ich verstehe was du meinst.
Doval
Eine andere Sache: Ein "abstrakter Datentyp" ist ein informeller Begriff, ähnlich wie Dokumentation; Um es maschinenprüfbar zu machen, müssen Sie ein Typsystem erstellen und das Äquivalent der Schnittstellen dieses Typsystems verwenden.
@delnan Das ist ein Missverständnis. Siehe Informationen zum Verständnis der Datenabstraktion, überarbeitet . Ein ADT abstrahiert Daten wie eine Schnittstelle, ja. Ein ADT verschleiert jedoch die Identität eines vorhandenen Typs. Dies entspricht einer Klasse mit privaten Feldern im Mainstream-OOP. Der verdeckte Typ ist der Datensatz, dessen Felder Sie privat gemacht haben. Eine Schnittstelle hingegen ist ein Bündel von Funktionen, und der Implementierungstyp erscheint nirgends in den Signaturen. Dies ermöglicht mehrere Implementierungen und Substituierbarkeit.
Doval
Dies scheint eine etwas andere Verwendung des "abstrakten Datentyps" zu sein, als ich es gewohnt bin; Was ich und viele andere als abstrakten Datentyp kennen, ist nur ein Werkzeug, um sprachunabhängig über Datentypen zu sprechen. Was Sie beschreiben, scheint eine (Klasse von) Typ-Systemfunktion (en) zu sein, die den oben genannten Begriff der abstrakten Datentypen interpretieren und implementieren.
2

Es gibt keine Möglichkeit zu 100% Kraft des Verhalten , das Sie wollen, aber unter Verwendung von Code Verträge könnte man sagen , dass Implementierungen nicht null zurückgeben muß, und vielleicht , dass das Benutzerobjekt die ID übergeben hat (so ein Objekt mit Benutzer - ID 0 würde scheitern , wenn 1 übergeben wurde ). Darüber hinaus kann es hilfreich sein, zu dokumentieren, was Sie von Implementierungen erwarten.

Andy
quelle
0

Dies ist wahrscheinlich sehr spät, um diese Frage zu beantworten, möchte aber meine Gedanken dazu teilen. Wie aus allen Antworten hervorgeht, ist es nicht möglich, die Einschränkung mit der Schnittstelle zu erzwingen. Auch mit Hacks wäre das schwierig.

Eine Möglichkeit, dies zu erreichen, die wahrscheinlich spezifisch für .NET ist, besteht darin, die Benutzeroberfläche wie folgt zu gestalten:

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

Die Task erzwingt nicht das Auslösen bestimmter Ausnahmetypen, bietet jedoch die Möglichkeit, die Absicht aller Implementierer dieser Schnittstelle zu vermitteln. Aufgaben in .NET haben ein bestimmtes Verwendungsmuster, z. B. Task.Ergebnis für den Zugriff auf das Ergebnis, Task.Exception, wenn eine Ausnahme vorliegt. Außerdem kann der Benutzer es auch asynchron verwenden.

Bhalchandra K.
quelle
0

Mit einer Schnittstelle ist das nicht möglich, da viele andere bereits geantwortet haben.
Aber Sie können das mit einer abstrakten Basisklasse tun:

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