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 ?
quelle
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.Antworten:
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 :
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:
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:
Hier wird die
IReservationsRepository
Abhängigkeit ausschließlich vom Client, derMaîtreD
Klasse, bestimmt:Diese Schnittstelle ist vollständig synchron, da die
MaîtreD
Klasse sie nicht asynchron benötigen muss.Asynchrone Abhängigkeiten
Sie können die Schnittstelle einfach so ändern, dass sie asynchron ist:
Die
MaîtreD
Klasse 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. DieTryAccept
Methode muss nun auch asynchron werden: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.
quelle
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.
quelle
Das
async
Attribut 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,
async
die 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.
quelle
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).
quelle
Betrachten Sie die folgenden Beispiele:
Dies ist eine Methode, die den Namen festlegt, bevor er zurückgegeben wird:
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):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):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
async
Schlü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
async
Formen 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.quelle
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.
quelle
Hier ist ein entgegengesetzter Standpunkt.
Wir sind nicht von der Rückkehr
Foo
zur Rückkehr übergegangen,Task<Foo>
weil wir angefangen haben, dasTask
statt nur das zu wollenFoo
. Zugegeben, manchmal interagieren wir mit demTask
Code, aber in den meisten realen Codes ignorieren wir ihn und verwenden einfach denFoo
.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.
quelle
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-
Task
Typs 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 esIObservable
eher 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.quelle
Task<T>
bedeutet asynchron. Sie erhalten das Aufgabenobjekt sofort, müssen aber möglicherweise auf die Reihenfolge der Benutzer warten