Ist eine Schnittstelle, die asynchrone Funktionen verfügbar macht, eine undichte Abstraktion?

12

Ich lese das Buch Prinzipien, Praktiken und Muster der Abhängigkeitsinjektion und lese über das Konzept der undichten Abstraktion, das im Buch gut beschrieben wird.

In diesen Tagen überarbeite ich eine C # -Codebasis mithilfe der Abhängigkeitsinjektion, sodass asynchrone Aufrufe verwendet werden, anstatt solche zu blockieren. Dabei erwäge ich einige Schnittstellen, die Abstraktionen in meiner Codebasis darstellen und die neu gestaltet werden müssen, damit asynchrone Aufrufe verwendet werden können.

Betrachten Sie als Beispiel die folgende Schnittstelle, die ein Repository für Anwendungsbenutzer darstellt:

public interface IUserRepository 
{
  Task<IEnumerable<User>> GetAllAsync();
}

Gemäß der Buchdefinition ist eine undichte Abstraktion eine Abstraktion, die für eine bestimmte Implementierung entworfen wurde, so dass einige Implementierungsdetails durch die Abstraktion selbst "lecken".

Meine Frage lautet wie folgt: Können wir eine asynchrone Schnittstelle wie IUserRepository als Beispiel für eine undichte Abstraktion betrachten?

Natürlich haben nicht alle möglichen Implementierungen etwas mit Asynchronität zu tun: Nur die Out-of-Process-Implementierungen (z. B. eine SQL-Implementierung) tun dies, aber ein In-Memory-Repository erfordert keine Asynchronität (die tatsächliche Implementierung einer In-Memory-Version der Schnittstelle ist wahrscheinlich mehr Dies ist schwierig, wenn die Schnittstelle asynchrone Methoden verfügbar macht. Beispielsweise müssen Sie wahrscheinlich etwas wie Task.CompletedTask oder Task.FromResult (Benutzer) in den Methodenimplementierungen zurückgeben.

Was denkst du darüber ?

Enrico Massone
quelle
@Neil Ich habe wahrscheinlich verstanden. Eine Schnittstelle, die Methoden verfügbar macht, die Task oder Task <T> zurückgeben, ist an sich keine undichte Abstraktion, sondern lediglich ein Vertrag mit einer Signatur, die Tasks umfasst. Eine Methode, die eine Aufgabe oder Aufgabe <T> zurückgibt, impliziert keine asynchrone Implementierung (z. B. wenn ich eine abgeschlossene Aufgabe mithilfe von Task.CompletedTask erstelle, führe ich keine asynchrone Implementierung durch). Umgekehrt erfordert die asynchrone Implementierung in C #, dass der Rückgabetyp einer asynchronen Methode vom Typ Task oder Task <T> sein muss. Anders ausgedrückt, der einzige "undichte" Aspekt meiner Benutzeroberfläche ist das asynchrone Suffix in Namen
Enrico Massone
@Neil tatsächlich gibt es eine Namensrichtlinie, die besagt, dass alle asynchronen Methoden einen Namen haben sollten, der auf "Async" endet. Dies bedeutet jedoch nicht, dass eine Methode, die eine Aufgabe oder eine Aufgabe <T> zurückgibt, mit dem Async-Suffix benannt werden muss, da sie ohne Verwendung von asynchronen Aufrufen implementiert werden kann.
Enrico Massone
6
Ich würde argumentieren, dass die 'Asynchronität' einer Methode durch die Tatsache angezeigt wird, dass sie a zurückgibt Task. Die Richtlinien zum Suffixieren von asynchronen Methoden mit dem Wort asynchron bestanden darin, zwischen ansonsten identischen API-Aufrufen zu unterscheiden (C # kann nicht basierend auf dem Rückgabetyp versendet werden). In unserer Firma haben wir alles zusammen fallen lassen.
Richzilla
Es gibt eine Reihe von Antworten und Kommentaren, die erklären, warum die asynchrone Natur der Methode Teil der Abstraktion ist. Eine interessantere Frage ist, wie eine Sprache oder Programmier-API die Funktionalität einer Methode von der Ausführung trennen kann, bis zu dem Punkt, an dem wir keine Task-Rückgabewerte oder asynchrone Markierungen mehr benötigen. Die Leute von der funktionalen Programmierung scheinen dies besser herausgefunden zu haben. Überlegen Sie, wie asynchrone Methoden in F # und anderen Sprachen definiert sind.
Frank Hileman
2
:-) -> Die "funktionalen Programmierer" ha. Async ist nicht undichter als synchron, es scheint nur so, weil wir es gewohnt sind, standardmäßig Sync-Code zu schreiben. Wenn wir alle standardmäßig asynchron codieren, scheint eine synchrone Funktion undicht zu sein.
StarTrekRedneck

Antworten:

8

Man kann sich natürlich auf das Gesetz der undichten Abstraktionen berufen , aber das ist nicht besonders interessant, da es davon ausgeht, dass alle Abstraktionen undicht sind. Man kann für und gegen diese Vermutung argumentieren, aber es hilft nicht, wenn wir nicht verstehen, was wir unter Abstraktion verstehen und was wir unter undicht verstehen . Daher werde ich zunächst versuchen zu beschreiben, wie ich jeden dieser Begriffe betrachte:

Abstraktionen

Meine Lieblingsdefinition von Abstraktionen leitet sich aus der APPP von Robert C. Martin ab :

"Eine Abstraktion ist die Verstärkung des Wesentlichen und die Beseitigung des Irrelevanten."

Somit Schnittstellen sind nicht in sich selbst, Abstraktionen . Sie sind nur dann Abstraktionen, wenn sie das Wesentliche an die Oberfläche bringen und den Rest verbergen.

Undicht

Das Buch Prinzipien, Muster und Praktiken der Abhängigkeitsinjektion definiert den Begriff undichte Abstraktion im Kontext der Abhängigkeitsinjektion (DI). Polymorphismus und die SOLID-Prinzipien spielen in diesem Zusammenhang eine große Rolle.

Aus dem Dependency Inversion Principle (DIP) folgt unter erneuter Angabe von APPP:

"Kunden besitzen [...] die abstrakten Schnittstellen"

Dies bedeutet, dass Clients (aufrufender Code) die erforderlichen Abstraktionen definieren und Sie diese Abstraktion dann implementieren.

Eine undichte Abstraktion ist meiner Ansicht nach eine Abstraktion, die gegen das DIP verstößt, indem sie einige Funktionen enthält, die der Client nicht benötigt .

Synchrone Abhängigkeiten

Ein Client, der eine Geschäftslogik implementiert, verwendet DI normalerweise, um sich von bestimmten Implementierungsdetails zu entkoppeln, z. B. von Datenbanken.

Stellen Sie sich ein Domain-Objekt vor, das eine Anfrage für eine Restaurantreservierung bearbeitet:

public class MaîtreD : IMaîtreD
{
    public MaîtreD(int capacity, IReservationsRepository repository)
    {
        Capacity = capacity;
        Repository = repository;
    }

    public int Capacity { get; }
    public IReservationsRepository Repository { get; }

    public int? TryAccept(Reservation reservation)
    {
        var reservations = Repository.ReadReservations(reservation.Date);
        int reservedSeats = reservations.Sum(r => r.Quantity);

        if (Capacity < reservedSeats + reservation.Quantity)
            return null;

        reservation.IsAccepted = true;
        return Repository.Create(reservation);
    }
}

Hier wird die IReservationsRepositoryAbhängigkeit ausschließlich vom Client, der MaîtreDKlasse, bestimmt:

public interface IReservationsRepository
{
    Reservation[] ReadReservations(DateTimeOffset date);
    int Create(Reservation reservation);
}

Diese Schnittstelle ist vollständig synchron, da die MaîtreDKlasse sie nicht asynchron benötigen muss.

Asynchrone Abhängigkeiten

Sie können die Schnittstelle einfach so ändern, dass sie asynchron ist:

public interface IReservationsRepository
{
    Task<Reservation[]> ReadReservations(DateTimeOffset date);
    Task<int> Create(Reservation reservation);
}

Die MaîtreDKlasse benötigt diese Methoden jedoch nicht , um asynchron zu sein, sodass jetzt das DIP verletzt wird. Ich halte dies für eine undichte Abstraktion, da ein Implementierungsdetail den Client zu Änderungen zwingt. Die TryAcceptMethode muss nun auch asynchron werden:

public async Task<int?> TryAccept(Reservation reservation)
{
    var reservations =
        await Repository.ReadReservations(reservation.Date);
    int reservedSeats = reservations.Sum(r => r.Quantity);

    if (Capacity < reservedSeats + reservation.Quantity)
        return null;

    reservation.IsAccepted = true;
    return await Repository.Create(reservation);
}

Es gibt keine inhärente Begründung dafür, dass die Domänenlogik asynchron ist. Um jedoch die Asynchronität der Implementierung zu unterstützen, ist dies jetzt erforderlich.

Bessere Optionen

Auf der NDC Sydney 2018 hielt ich einen Vortrag zu diesem Thema . Darin skizziere ich auch eine Alternative, die nicht leckt. Ich werde diesen Vortrag auch 2019 auf mehreren Konferenzen halten, aber jetzt mit dem neuen Titel Async Injection umbenannt .

Ich plane, auch eine Reihe von Blog-Posts zu veröffentlichen, um den Vortrag zu begleiten. Diese Artikel sind bereits geschrieben und befinden sich in meiner Artikelwarteschlange und warten darauf, veröffentlicht zu werden. Bleiben Sie also auf dem Laufenden.

Mark Seemann
quelle
Meiner Meinung nach ist dies eine Absichtssache. Wenn meine Abstraktion so aussieht, als ob sie sich in eine Richtung verhalten sollte, aber ein Detail oder eine Einschränkung die Abstraktion wie dargestellt unterbricht, ist dies eine undichte Abstraktion. In diesem Fall stelle ich Ihnen jedoch ausdrücklich vor, dass die Operation asynchron ist - das versuche ich nicht zu abstrahieren. Das unterscheidet sich in meinen Augen von Ihrem Beispiel, in dem ich (mit Bedacht oder nicht) versuche, die Tatsache zu abstrahieren, dass es eine SQL-Datenbank gibt und ich immer noch eine Verbindungszeichenfolge verfügbar mache. Vielleicht ist es eine Frage der Semantik / Perspektive.
Ameise P
Wir können also sagen, dass eine Abstraktion niemals "per se" undicht ist, sondern eine undichte, wenn einige Details einer bestimmten Implementierung aus den exponierten Mitgliedern austreten und den Verbraucher dazu zwingen, ihre Implementierung zu ändern, um die Abstraktionsform zu erfüllen .
Enrico Massone
2
Interessanterweise ist der Punkt, den Sie in Ihrer Erklärung hervorgehoben haben, einer der am meisten missverstandenen Punkte der gesamten Geschichte der Abhängigkeitsinjektion. Manchmal vergessen Entwickler das Prinzip der Abhängigkeitsinversion und versuchen zuerst, die Abstraktion zu entwerfen, und passen dann das Consumer-Design an, um mit der Abstraktion selbst fertig zu werden. Stattdessen sollte der Vorgang in umgekehrter Reihenfolge durchgeführt werden.
Enrico Massone
11

Es ist überhaupt keine undichte Abstraktion.

Asynchron zu sein ist eine grundlegende Änderung der Definition einer Funktion. Dies bedeutet, dass die Aufgabe nicht beendet ist, wenn der Aufruf zurückkehrt, aber es bedeutet auch, dass Ihr Programmablauf fast sofort und nicht mit einer längeren Verzögerung fortgesetzt wird. Eine asynchrone und eine synchrone Funktion, die dieselbe Aufgabe ausführen, sind im Wesentlichen unterschiedliche Funktionen. Asynchron zu sein ist kein Implementierungsdetail. Es ist Teil der Definition einer Funktion.

Wenn die Funktion offenlegen würde, wie die Funktion asynchron gemacht wurde, wäre dies undicht. Sie (müssen / sollten nicht) sich darum kümmern, wie es implementiert wird.

gnasher729
quelle
5

Das asyncAttribut einer Methode ist ein Tag, das angibt, dass besondere Sorgfalt und Handhabung erforderlich sind. Als solches ist es muss in der Welt entweichen. Asynchrone Operationen sind äußerst schwierig zu komponieren, daher ist es wichtig, dem API-Benutzer ein Heads-up zu geben.

Wenn Ihre Bibliothek stattdessen alle asynchronen Aktivitäten in sich selbst ordnungsgemäß verwaltet, können Sie es sich leisten, asyncdie API nicht "auslaufen" zu lassen .

Bei Software gibt es vier Schwierigkeitsdimensionen: Daten, Steuerung, Raum und Zeit. Asynchrone Operationen erstrecken sich über alle vier Dimensionen und erfordern daher die größte Sorgfalt.

BobDalgleish
quelle
Ich stimme Ihrem Gefühl zu, aber "Leck" impliziert etwas Schlechtes, was die Absicht des Begriffs "undichte Abstraktion" ist - etwas Unerwünschtes in der Abstraktion. Im Fall von Async vs Sync ist nichts undicht.
StarTrekRedneck
2

Eine undichte Abstraktion ist eine Abstraktion, die für eine bestimmte Implementierung entwickelt wurde, sodass einige Implementierungsdetails durch die Abstraktion selbst "lecken".

Nicht ganz. Eine Abstraktion ist eine konzeptionelle Sache, die einige Elemente einer komplizierteren konkreten Sache oder eines Problems ignoriert (um die Sache / das Problem einfacher, nachvollziehbarer oder aufgrund eines anderen Vorteils zu machen). Als solches unterscheidet es sich notwendigerweise von der eigentlichen Sache / dem eigentlichen Problem, und daher wird es in einigen Teilmengen von Fällen undicht sein (dh alle Abstraktionen sind undicht, die einzige Frage ist, inwieweit - was bedeutet, in welchen Fällen ist die Abstraktion nützlich für uns, was ist ihr Anwendungsbereich).

Das heißt, wenn es um Software-Abstraktionen geht, können die Details, die wir ignoriert haben, manchmal (oder vielleicht oft genug?) Nicht ignoriert werden, da sie einen für uns wichtigen Aspekt der Software beeinflussen (Leistung, Wartbarkeit, ...). . Eine undichte Abstraktion ist also eine Abstraktion, die entwickelt wurde, um bestimmte Details zu ignorieren (unter der Annahme, dass dies möglich und nützlich war), aber dann stellte sich heraus, dass einige dieser Details in der Praxis von Bedeutung sind (sie können nicht ignoriert werden, also sie) "Auslaufen").

So ist eine Schnittstelle ein Detail einer Implementierung auszusetzen ist nicht undicht per se (oder besser gesagt, eine Schnittstelle, isoliert betrachtet, ist nicht an sich eine undichte Abstraktion); Stattdessen hängt die Undichtigkeit vom Code ab, der die Schnittstelle implementiert (kann sie tatsächlich die von der Schnittstelle dargestellte Abstraktion unterstützen), und auch von den Annahmen des Client-Codes (die eine konzeptionelle Abstraktion darstellen, die die von ausgedrückte ergänzt) Die Benutzeroberfläche kann jedoch nicht selbst in Code ausgedrückt werden (z. B. sind die Funktionen der Sprache nicht ausdrucksstark genug, sodass wir sie möglicherweise in den Dokumenten usw. beschreiben).

Filip Milovanović
quelle
2

Betrachten Sie die folgenden Beispiele:

Dies ist eine Methode, die den Namen festlegt, bevor er zurückgegeben wird:

public void SetName(string name)
{
    _dataLayer.SetName(name);
}

Dies ist eine Methode, die den Namen festlegt. Der Anrufer kann nicht davon ausgehen, dass der Name festgelegt ist, bis die zurückgegebene Aufgabe abgeschlossen ist ( IsCompleted= true):

public Task SetName(string name)
{
    return _dataLayer.SetNameAsync(name);
}

Dies ist eine Methode, die den Namen festlegt. Der Anrufer kann nicht davon ausgehen, dass der Name festgelegt ist, bis die zurückgegebene Aufgabe abgeschlossen ist ( IsCompleted= true):

public async Task SetName(string name)
{
    await _dataLayer.SetNameAsync(name);
}

F: Welches gehört nicht zu den anderen beiden?

A: Die asynchrone Methode ist nicht die, die alleine steht. Die Methode, die für sich allein steht, ist die Methode, die void zurückgibt.

Für mich ist das "Leck" hier nicht das asyncSchlüsselwort; Es ist die Tatsache, dass die Methode eine Aufgabe zurückgibt. Und das ist kein Leck; Es ist Teil des Prototyps und Teil der Abstraktion. Eine asynchrone Methode, die eine Aufgabe zurückgibt, macht genau das gleiche Versprechen wie eine synchrone Methode, die eine Aufgabe zurückgibt.

Also nein, ich denke nicht, dass die Einführung von asyncFormen eine undichte Abstraktion an und für sich ist. Möglicherweise müssen Sie jedoch den Prototyp ändern, um eine Aufgabe zurückzugeben, die durch Ändern der Schnittstelle (der Abstraktion) "leckt". Und da es Teil der Abstraktion ist, ist es per Definition kein Leck.

John Wu
quelle
0

Dies ist genau dann eine undichte Abstraktion, wenn nicht alle implementierenden Klassen einen asynchronen Aufruf erstellen sollen. Sie können mehrere Implementierungen erstellen, z. B. eine für jeden von Ihnen unterstützten Datenbanktyp. Dies wäre völlig in Ordnung, vorausgesetzt, Sie müssten nie die genaue Implementierung kennen, die in Ihrem Programm verwendet wird.

Und obwohl Sie eine asynchrone Implementierung nicht strikt erzwingen können, impliziert der Name, dass dies der Fall sein sollte. Wenn sich die Umstände ändern und es sich aus irgendeinem Grund um einen synchronen Anruf handelt, müssen Sie möglicherweise eine Namensänderung in Betracht ziehen. Mein Rat wäre daher, dies nur zu tun, wenn Sie nicht der Meinung sind, dass dies in der EU sehr wahrscheinlich ist Zukunft.

Neil
quelle
0

Hier ist ein entgegengesetzter Standpunkt.

Wir sind nicht von der Rückkehr Foozur Rückkehr übergegangen, Task<Foo>weil wir angefangen haben, das Taskstatt nur das zu wollen Foo. Zugegeben, manchmal interagieren wir mit dem TaskCode, aber in den meisten realen Codes ignorieren wir ihn und verwenden einfach den Foo.

Darüber hinaus definieren wir häufig Schnittstellen, um das asynchrone Verhalten zu unterstützen, selbst wenn die Implementierung asynchron ist oder nicht.

Tatsächlich Task<Foo>teilt Ihnen eine Schnittstelle, die a zurückgibt , mit, dass die Implementierung möglicherweise asynchron ist, unabhängig davon, ob dies tatsächlich der Fall ist oder nicht, auch wenn Sie sich möglicherweise darum kümmern oder nicht. Wenn eine Abstraktion mehr sagt, als wir über ihre Implementierung wissen müssen, ist sie undicht.

Wenn unsere Implementierung nicht asynchron ist, ändern wir sie in asynchron, und dann müssen wir die Abstraktion und alles, was sie verwendet, ändern. Das ist eine sehr undichte Abstraktion.

Das ist kein Urteil. Wie andere betont haben, lecken alle Abstraktionen. Dieser hat eine größere Auswirkung, da er einen Ripple-Effekt von async / awaits in unserem gesamten Code erfordert, nur weil irgendwo am Ende möglicherweise etwas vorhanden ist, das tatsächlich asynchron ist.

Klingt das nach einer Beschwerde? Das ist nicht meine Absicht, aber ich denke, es ist eine genaue Beobachtung.

Ein verwandter Punkt ist die Behauptung, dass "eine Schnittstelle keine Abstraktion ist". Was Mark Seeman kurz und bündig sagte, wurde ein wenig missbraucht.

Die Definition von "Abstraktion" ist auch in .NET nicht "Schnittstelle". Abstraktionen können viele andere Formen annehmen. Eine Schnittstelle kann eine schlechte Abstraktion sein oder ihre Implementierung so genau widerspiegeln, dass sie in gewissem Sinne kaum eine Abstraktion ist.

Wir verwenden jedoch unbedingt Schnittstellen, um Abstraktionen zu erstellen. "Schnittstellen sind keine Abstraktionen" wegzuwerfen, weil in einer Frage Schnittstellen erwähnt werden und Abstraktionen nicht aufschlussreich sind.

Scott Hannen
quelle
-2

Ist GetAllAsync()eigentlich asynchron? Ich meine sicher, dass "async" im Namen ist, aber das kann entfernt werden. Also frage ich noch einmal ... Ist es unmöglich, eine Funktion zu implementieren, die eine zurückgibt, Task<IEnumerable<User>>die synchron aufgelöst wird?

Ich kenne die Besonderheiten des .Net- TaskTyps nicht, aber wenn es unmöglich ist, die Funktion synchron zu implementieren, ist es sicher eine undichte Abstraktion (auf diese Weise), aber ansonsten nicht. Ich weiß , dass wenn es IObservableeher eine als eine Aufgabe wäre, sie entweder synchron oder asynchron implementiert werden könnte , so dass nichts außerhalb der Funktion weiß und daher diese bestimmte Tatsache nicht verloren geht.

Daniel T.
quelle
Task<T> bedeutet asynchron. Sie erhalten das Aufgabenobjekt sofort, müssen aber möglicherweise auf die Reihenfolge der Benutzer warten
Caleth
Möglicherweise muss warten, was nicht bedeutet, dass es unbedingt asynchron ist. Soll bedeuten würde Asynchron warten. Wenn die zugrunde liegende Aufgabe bereits ausgeführt wurde, müssen Sie vermutlich nicht warten.
Daniel T.