Hallo allerseits!
Wir haben endlich einen Vertrag, um Mark Simans Buch "
Dependency Injection in .NET " zu aktualisieren - Hauptsache, er beendet es so schnell wie möglich. Wir haben auch ein
Buch im Herausgeber des angesehenen Dinesh Rajput über Entwurfsmuster im Frühjahr 5, in dem eines der Kapitel auch der Implementierung von Abhängigkeiten gewidmet ist.
Wir haben lange nach interessantem Material gesucht, das an die Stärken des DI-Paradigmas erinnert und unser Interesse daran verdeutlicht - und jetzt wurde es gefunden. Der Autor zog es vor, Beispiele in Go zu nennen. Wir hoffen, dass dies Sie nicht daran hindert, seinen Gedanken zu folgen, und hilft, die allgemeinen Prinzipien der Steuerungsinversion und der Arbeit mit Schnittstellen zu verstehen, wenn dieses Thema in Ihrer Nähe ist.
Die emotionale Färbung des Originals ist etwas leiser, die Anzahl der Ausrufezeichen in der Übersetzung ist reduziert. Viel Spaß beim Lesen!
Die Verwendung von
Schnittstellen ist eine verständliche Technik, mit der Sie Code erstellen können, der einfach zu testen und leicht erweiterbar ist. Ich war wiederholt davon überzeugt, dass dies das leistungsstärkste Architektur-Design-Tool von allen ist.
In diesem Artikel wird erläutert, was Schnittstellen sind, wie sie verwendet werden und wie sie die Erweiterbarkeit und Testbarkeit von Code bieten. Schließlich sollte der Artikel zeigen, wie Schnittstellen dazu beitragen können, das Software Delivery Management zu optimieren und die Planung zu vereinfachen!
SchnittstellenDie Schnittstelle beschreibt den Vertrag. Abhängig von der Sprache oder dem Framework kann die Verwendung von Schnittstellen explizit oder implizit diktiert werden. In der Go-Sprache werden
Schnittstellen also explizit diktiert . Wenn Sie versuchen, eine Entität als Schnittstelle zu verwenden, diese jedoch nicht vollständig mit den Regeln dieser Schnittstelle übereinstimmt, tritt ein Fehler bei der Kompilierung auf. Wenn Sie beispielsweise das obige Beispiel ausführen, wird der folgende Fehler angezeigt:
prog.go:22:85: cannot use BadPricer literal (type BadPricer) as type StockPricer in argument to isPricerHigherThan100: BadPricer does not implement StockPricer (missing CurrentPrice method) Program exited.
Interfaces ist ein Tool, mit dem der Anrufer vom Angerufenen getrennt werden kann. Dies erfolgt über einen Vertrag.
Lassen Sie uns dieses Problem anhand eines Beispiels für ein Programm für den automatischen Börsenhandel konkretisieren. Das Händlerprogramm wird mit einem festgelegten Kaufpreis und einem Tickersymbol aufgerufen. Dann geht das Programm zur Börse, um das aktuelle Angebot dieses Tickers herauszufinden. Wenn der Kaufpreis für diesen Ticker den festgelegten Preis nicht überschreitet, wird das Programm einen Kauf tätigen.

In vereinfachter Form kann die Architektur dieses Programms wie folgt dargestellt werden. Aus dem obigen Beispiel ist klar, dass der Vorgang des Erhaltens des aktuellen Preises direkt vom HTTP-Protokoll abhängt, über das das Programm den Austauschdienst kontaktiert.
Der Status der
Action
auch direkt von HTTP ab. Daher sollten beide Staaten vollständig verstehen, wie HTTP zum Extrahieren von Austauschdaten und / oder zum Abschließen von Transaktionen verwendet wird.
So könnte die Implementierung aussehen:
func analyze(ticker string, maxTradePrice float64) (bool, err) { resp, err := http.Get( "http://stock-service.com/currentprice/" + ticker ) if err != nil {
Hier ist der Anrufer (
analyze
) direkt von HTTP abhängig. Sie muss wissen, wie HTTP-Anforderungen formuliert sind. Wie wird sie analysiert? Umgang mit Wiederholungsversuchen, Zeitüberschreitungen, Authentifizierung usw. Sie hat
http
Griff .
Wann immer wir analyse aufrufen, müssen wir auch die http
Bibliothek aufrufen .
Wie kann uns die Schnittstelle hier helfen? In dem von der Schnittstelle bereitgestellten Vertrag können Sie das
Verhalten und nicht die spezifische
Implementierung beschreiben .
type StockExchange interface { CurrentPrice(ticker string) float64 }
Das Obige definiert das Konzept von
StockExchange
. Hier heißt es, dass
StockExchange
das Aufrufen der einzigen
CurrentPrice
Funktion unterstützt. Diese drei Linien scheinen mir die mächtigste Architekturtechnik von allen zu sein. Sie helfen uns, Anwendungsabhängigkeiten viel sicherer zu steuern. Testen. Erweiterbarkeit bereitstellen.
AbhängigkeitsinjektionUm den Wert von Schnittstellen vollständig zu verstehen, müssen Sie die als „Abhängigkeitsinjektion“ bezeichnete Technik beherrschen.
Abhängigkeitsinjektion bedeutet, dass der Anrufer etwas bereitstellt, das der Anrufer benötigt. Normalerweise sieht es so aus: Der Anrufer konfiguriert das Objekt und gibt es dann an den Angerufenen weiter. Dann abstrahiert der angerufene Teilnehmer von der Konfiguration und Implementierung. In diesem Fall ist eine Mediation bekannt. Betrachten Sie eine Anfrage an den HTTP Rest-Dienst. Um den Client zu implementieren, müssen wir eine HTTP-Bibliothek verwenden, die HTTP-Anforderungen formulieren, senden und empfangen kann.
Wenn wir die HTTP-Anfrage hinter die Schnittstelle stellen würden, könnte der Anrufer getrennt werden, und er würde "nicht wissen", dass die HTTP-Anfrage tatsächlich stattgefunden hat.
Der Aufrufer sollte nur einen generischen Funktionsaufruf durchführen. Dies kann ein lokaler Anruf, ein Fernanruf, ein HTTP-Anruf, ein RPC-Anruf usw. sein. Der Anrufer weiß nicht, was passiert, und normalerweise passt es perfekt zu ihm, solange er die erwarteten Ergebnisse erzielt. Das Folgende zeigt, wie die Abhängigkeitsinjektion in unserer
analyze
aussehen könnte.
func analyze(se StockExchange, ticker string, maxTradePrice float64) (bool, error) { currentPrice := se.CurrentPrice(ticker) var hasTraded bool var err error if currentPrice <= maximumTradePrice { err = doTrade(ticker, currentPrice) if err == nil { hasTraded = true } } return hasTraded, err }
Ich bin immer wieder erstaunt darüber, was hier passiert. Wir haben unseren Abhängigkeitsbaum vollständig umgekehrt und begonnen, das gesamte Programm besser zu steuern. Darüber hinaus ist die gesamte Implementierung auch optisch sauberer und verständlicher geworden. Wir sehen deutlich, dass die Analysemethode den aktuellen Preis auswählen, prüfen sollte, ob dieser Preis für uns geeignet ist, und wenn ja, einen Deal abschließen sollte.
Am wichtigsten ist, dass wir in diesem Fall den Anrufer vom Anrufer trennen. Da der Aufrufer und die gesamte Implementierung über die Schnittstelle von der aufgerufenen getrennt sind, können Sie die Schnittstelle erweitern, indem Sie viele verschiedene Implementierungen davon erstellen. Mit Schnittstellen können Sie viele verschiedene spezifische Implementierungen erstellen, ohne den Code des angerufenen Teilnehmers ändern zu müssen!

Der Status "Aktuellen Preis
StockExchange
" in diesem Programm hängt nur von der
StockExchange
Oberfläche ab. Diese Implementierung weiß
nichts darüber, wie man den Austauschdienst kontaktiert, wie Preise gespeichert werden oder wie Anfragen gestellt werden. Wirklich glückselige Unwissenheit. Darüber hinaus bilateral. Die
HTTPStockExchange
Implementierung weiß auch nichts über die Analyse. Über den Kontext, in dem die Analyse durchgeführt wird, wenn sie durchgeführt wird - weil die Herausforderungen indirekt auftreten.
Da Programmfragmente (solche, die von Schnittstellen abhängen) beim Ändern / Hinzufügen / Löschen bestimmter Implementierungen nicht geändert werden müssen,
erweist sich ein solches Design als dauerhaft . Angenommen, wir stellen fest, dass
StockService
sehr oft nicht verfügbar ist.
Wie unterscheidet sich das obige Beispiel vom Aufruf einer Funktion? Wenn Sie einen Funktionsaufruf anwenden, wird auch die Implementierung sauberer. Der Unterschied besteht darin, dass wir beim Aufrufen der Funktion immer noch auf HTTP zurückgreifen müssen. Die
analyze
delegiert einfach die Aufgabe der Funktion, die
http
aufrufen soll, anstatt
http
selbst direkt aufzurufen. Die ganze Stärke dieser Technik liegt in der „Injektion“, dh darin, dass der Anrufer die Schnittstelle zum Angerufenen bereitstellt. Genau so kommt es zur Abhängigkeitsinversion, bei der die Abrufpreise nur von der Schnittstelle und nicht von der Implementierung abhängen.
Mehrere sofort einsatzbereite ImplementierungenZu diesem Zeitpunkt haben wir die
analyze
und die
StockExchange
Schnittstelle, aber wir können tatsächlich nichts Nützliches tun. Habe gerade unser Programm angekündigt. Im Moment ist es unmöglich, es aufzurufen, da wir noch keine einzige spezifische Implementierung haben, die die Anforderungen unserer Schnittstelle erfüllen würde.
Das Hauptaugenmerk im folgenden Diagramm liegt auf dem
StockExchange
"Aktuellen Preis
StockExchange
" und seiner Abhängigkeit von der
StockExchange
Schnittstelle. Das Folgende zeigt, wie zwei völlig unterschiedliche Implementierungen nebeneinander existieren und der aktuelle Preis nicht bekannt ist. Darüber hinaus sind beide Implementierungen nicht miteinander verbunden, sondern hängen jeweils nur von der
StockExchange
Schnittstelle ab.

Produktion
Die ursprüngliche HTTP-Implementierung ist bereits in der primären
analyze
. Alles, was uns bleibt, ist, es zu extrahieren und hinter einer konkreten Implementierung der Schnittstelle zu kapseln.
type HTTPStockExchange struct {} func (se HTTPStockExchange) CurrentPrice(ticker string) float64 { resp, err := http.Get( "http://stock-service.com/currentprice/" + ticker ) if err != nil {
Der Code, den wir zuvor mit der Analysefunktion verknüpft haben, ist jetzt autonom und erfüllt die
StockExchange
Schnittstelle. Das heißt, wir können ihn jetzt zur
analyze
. Wie Sie sich aus den obigen Diagrammen erinnern, ist die Analyse nicht mehr mit der HTTP-Abhängigkeit verbunden. Über die Benutzeroberfläche kann sich
analyze
nicht vorstellen, was hinter den Kulissen passiert. Er weiß nur, dass ihm garantiert ein Objekt gegeben wird, mit dem er
CurrentPrice
aufrufen
CurrentPrice
.
Auch hier nutzen wir die typischen Vorteile der Einkapselung. Früher, als http-Anfragen an die Analyse gebunden waren, war die einzige Möglichkeit, über http mit dem Austausch zu kommunizieren, indirekt - über die
analyze
. Ja, wir könnten diese Aufrufe in Funktionen kapseln und die Funktion unabhängig ausführen, aber die Schnittstellen zwingen uns, den Anrufer vom Anrufer zu trennen. Jetzt können wir
HTTPStockExchange
unabhängig vom Aufrufer testen. Dies wirkt sich grundlegend auf den Umfang unserer Tests aus und darauf, wie wir Testfehler verstehen und darauf reagieren.
TestenIm vorhandenen Code haben wir die
HTTPStockService
Struktur, mit der wir separat sicherstellen können, dass sie mit dem Austauschdienst kommunizieren und die von ihm empfangenen Antworten analysieren kann.
StockExchange
wir nun sicher, dass die Analyse die Antwort von der
StockExchange
Schnittstelle korrekt verarbeiten kann und dass dieser Vorgang zuverlässig und reproduzierbar ist.
currentPrice := se.CurrentPrice(ticker) if currentPrice <= maxTradePrice { err := doTrade(ticker, currentPrice) }
Wir könnten die Implementierung mit HTTP verwenden, aber es hätte viele Nachteile. Das Tätigen von Netzwerkanrufen beim Testen von Einheiten kann insbesondere bei externen Diensten langsam sein. Aufgrund von Verzögerungen und einer instabilen Netzwerkverbindung können die Tests unzuverlässig sein. Wenn wir außerdem Tests mit der Aussage benötigen, dass wir die Transaktion abschließen können, und Tests mit der Aussage, dass wir Fälle herausfiltern können, in denen die Transaktion NICHT abgeschlossen werden sollte, wäre es schwierig, echte Produktionsdaten zu finden, die beide zuverlässig erfüllen Bedingungen. Man könnte
maxTradePrice
wählen und jede der Bedingungen auf diese Weise künstlich imitieren, zum Beispiel mit
maxTradePrice := -100
Transaktion nicht abgeschlossen werden und
maxTradePrice := 10000000
sollte offensichtlich mit der Transaktion enden.
Aber was passiert, wenn uns im Austauschdienst ein bestimmtes Kontingent zugewiesen wird? Oder ob wir den Zugang bezahlen müssen? Werden (und sollten) wir unsere Quote wirklich bezahlen oder ausgeben, wenn es um Unit-Tests geht? Im Idealfall sollten Tests so oft wie möglich durchgeführt werden, damit sie schnell, kostengünstig und zuverlässig sind. Ich denke, aus diesem Absatz geht hervor, warum die Verwendung einer Version mit reinem HTTP in Bezug auf Tests irrational ist!
Es gibt einen besseren Weg, und es beinhaltet die Verwendung von Schnittstellen!Mit einer Schnittstelle können Sie die
StockExchange
Implementierung sorgfältig herstellen,
StockExchange
wir schnell, sicher und zuverlässig
analyze
können.
type StubExchange struct { Price float64 } func (se StubExchange) CurrentPrice(ticker string) float64 { return se.Price } func TestAnalyze_MakeTrade(t *testing.T) { se := StubExchange{Price: 10} maxTradePrice := 11 traded, err := analyze(se, "TSLA", maxTradePrice) if err != nil { t.Errorf("expected err == nil received: %s", err) } if !traded { t.Error("expected traded == true") } } func TestAnalyze_DontTrade(t *testing.T) { se := StubExchange{Price: 10} maxTradePrice := 9 traded, err := analyze(se, "TSLA", maxTradePrice)
Der Stub des Austauschdienstes wird oben verwendet, wodurch der für uns interessante Zweig der
analyze
gestartet wird. Anschließend werden in jedem Test Aussagen getroffen, um sicherzustellen, dass die Analyse das tut, was benötigt wird. Obwohl dies ein Testprogramm ist, deutet meine Erfahrung darauf hin, dass Komponenten / Architekturen, bei denen die Schnittstellen ungefähr auf diese Weise verwendet werden, auf diese Weise auch auf Haltbarkeit im Kampfcode getestet werden !!! Dank der Schnittstellen können wir den im Speicher gesteuerten
StockExchange
, der zuverlässige, leicht konfigurierbare, leicht verständliche, reproduzierbare und blitzschnelle Tests bietet !!!
Unpin - AnruferkonfigurationNachdem wir nun besprochen haben, wie Schnittstellen verwendet werden, um den Anrufer vom Angerufenen zu trennen, und wie mehrere Implementierungen durchgeführt werden, haben wir noch keinen kritischen Aspekt angesprochen. Wie kann eine bestimmte Implementierung zu einem genau definierten Zeitpunkt konfiguriert und bereitgestellt werden? Sie können die Analysefunktion direkt aufrufen, aber was ist in der Produktionskonfiguration zu tun?
Hier bietet sich die Implementierung von Abhängigkeiten an.
func main() { var ticker = flag.String("ticker", "", "stock ticker symbol to trade for") var maxTradePrice = flag.Float64("maxtradeprice", "", "max price to pay for a share of the ticker symbol." se := HTTPStockExchange{} analyze(se, *ticker, *maxTradePrice) }
Genau wie in unserem Testfall wird die spezifische konkrete Implementierung von StockExchange, die mit
analyze
wird, vom Aufrufer außerhalb von analyse konfiguriert. Dann wird es zur
analyze
übergeben (injiziert). Dadurch wird sichergestellt, dass bei der Analyse von NICHTS bekannt ist, wie
HTTPStockExchange
konfiguriert ist. Vielleicht möchten wir die http-Domäne, die wir verwenden möchten, in Form eines Befehlszeilenflags bereitstellen, und dann muss die Analyse nicht geändert werden. Oder was tun, wenn wir eine Art Authentifizierung oder Token bereitstellen müssen, um auf
HTTPStockExchange
, das aus der Umgebung extrahiert wird? Auch hier sollte sich die Analyse nicht ändern.
Die Konfiguration erfolgt auf einer Ebene außerhalb der
analyze
, wodurch die Analyse vollständig von der Notwendigkeit befreit wird, ihre eigenen Abhängigkeiten zu konfigurieren. Damit wird eine strikte Aufgabentrennung erreicht.
Regale EntscheidungenVielleicht reichen die obigen Beispiele völlig aus, aber Schnittstellen und Abhängigkeitsinjektion bieten noch viele andere Vorteile. Schnittstellen ermöglichen es, Entscheidungen über bestimmte Implementierungen zu verschieben. Obwohl Entscheidungen erfordern, dass wir entscheiden, welches Verhalten wir unterstützen, können wir später Entscheidungen über bestimmte Implementierungen treffen. Angenommen, wir wussten, dass wir automatisierte Transaktionen durchführen wollten, waren uns aber noch nicht sicher, welchen Angebotsanbieter wir verwenden würden. Eine ähnliche Klasse von Lösungen wird bei der Arbeit mit Data Warehouses ständig behandelt. Was sollte unser Programm verwenden: MySQL, Postgres, Redis, Dateisystem, Cassandra? Letztendlich sind dies alles Implementierungsdetails, und die Schnittstellen ermöglichen es uns, endgültige Entscheidungen zu diesen Themen aufzuschieben. Sie ermöglichen es uns, die Geschäftslogik unserer Programme zu entwickeln und im letzten Moment auf bestimmte technologische Lösungen umzusteigen!
Trotz der Tatsache, dass diese Technik allein viele Möglichkeiten lässt, geschieht auf der Ebene der Projektplanung etwas Magisches. Stellen Sie sich vor, was passieren wird, wenn wir der Austauschschnittstelle eine weitere Abhängigkeit hinzufügen.

Hier werden wir unsere Architektur in Form eines gerichteten azyklischen Graphen neu konfigurieren, so dass wir, sobald wir uns auf die Details der Austauschschnittstelle einigen, mit
HTTPStockExchange
mit der Pipeline
HTTPStockExchange
. Wir haben eine Situation geschaffen, in der das Hinzufügen einer neuen Person zum Projekt uns hilft, schneller voranzukommen. Indem wir unsere Architektur auf diese Weise optimieren, sehen wir besser, wo, wann und wie lange wir zusätzliche Personen in das Projekt einbeziehen können, um die Bereitstellung des gesamten Projekts zu beschleunigen. Da die Verbindung zwischen unseren Schnittstellen schwach ist, ist es außerdem normalerweise einfach, sich an der Arbeit zu beteiligen, beginnend mit den Implementierungsschnittstellen. Sie können
HTTPStockExchange
völlig unabhängig von unserem Programm entwickeln, testen und testen!
Die Analyse architektonischer Abhängigkeiten und die Planung anhand dieser Abhängigkeiten können Projekte drastisch beschleunigen. Mit dieser speziellen Technik konnte ich sehr schnell Projekte abschließen, für die mehrere Monate vorgesehen waren.
Vor unsJetzt sollte klarer sein, wie die Schnittstellen und die Implementierung von Abhängigkeiten die Dauerhaftigkeit des entworfenen Programms sicherstellen. Angenommen, wir wechseln unseren Angebotsanbieter oder starten das Streaming von Kontingenten und speichern sie in Echtzeit. Es gibt so viele andere Möglichkeiten, wie Sie möchten. Die
StockExchange
in ihrer aktuellen Form unterstützt jede Implementierung, die für die Integration in die
StockExchange
Schnittstelle geeignet ist.
se.CurrentPrice(ticker)
So können Sie in vielen Fällen auf Änderungen verzichten. Nicht in allen, aber in den vorhersehbaren Fällen, denen wir begegnen können. Wir sind nicht nur immun gegen die Notwendigkeit, den
analyze
zu ändern und seine Schlüsselfunktionalität zu überprüfen, sondern können auch problemlos neue Implementierungen anbieten oder zwischen Lieferanten wechseln. Wir können auch die spezifischen Implementierungen, die wir bereits haben, problemlos erweitern oder aktualisieren, ohne die
analyze
ändern oder überprüfen zu müssen!
Ich hoffe, dass die obigen Beispiele überzeugend zeigen, wie die Schwächung der Kommunikation zwischen Entitäten im Programm durch die Verwendung von Schnittstellen die Abhängigkeiten vollständig neu ausrichtet und den Anrufer vom Anrufer trennt. Dank dieser Trennung hängt das Programm nicht von einer bestimmten Implementierung ab, sondern von einem bestimmten
Verhalten . Dieses Verhalten kann durch eine Vielzahl von Implementierungen bereitgestellt werden. Dieses kritische Konstruktionsprinzip wird auch als
Ententypisierung bezeichnet .
Das Konzept der Schnittstellen und die Abhängigkeit vom Verhalten und nicht von der Implementierung ist so mächtig, dass ich Schnittstellen als ein Sprachprimitiv betrachte - ja, das ist ziemlich radikal. Ich hoffe, dass sich die oben diskutierten Beispiele als ziemlich überzeugend erwiesen haben, und Sie werden zustimmen, dass die Schnittstellen und die Abhängigkeitsinjektion von Anfang an verwendet werden sollten. In fast allen Projekten, an denen ich gearbeitet habe, waren nicht eine, sondern mindestens zwei Implementierungen erforderlich: für die Produktion und zum Testen.