Verwendung von Schnittstellen in Go



In seiner Freizeit aus dem Hauptwerk konsultiert der Autor des Materials Go und analysiert den Code. Natürlich liest er im Verlauf einer solchen Aktivität viel Code, der von anderen Leuten geschrieben wurde. Vor kurzem hatte der Autor dieses Artikels den Eindruck (ja, der Eindruck, keine Statistik), dass Programmierer häufiger mit Schnittstellen im „Java-Stil“ zu arbeiten begannen.

Dieser Beitrag enthält Empfehlungen des Autors zur optimalen Verwendung von Schnittstellen in Go, basierend auf seiner Erfahrung beim Schreiben von Code.

In den Beispielen dieses Beitrags werden wir zwei Pakete animal und circus . Viele Dinge in diesem Beitrag beschreiben die Arbeit mit Code, der an die regelmäßige Verwendung von Paketen grenzt.

Wie man es nicht macht


Ein sehr häufiges Phänomen, das ich beobachte:

 package animals type Animal interface { Speaks() string } //  Animal type Dog struct{} func (a Dog) Speaks() string { return "woof" } 

 package circus import "animals" func Perform(a animal.Animal) string { return a.Speaks() } 

Dies ist die sogenannte Verwendung von Java-Schnittstellen. Es kann durch die folgenden Schritte charakterisiert werden:

  1. Definieren Sie eine Schnittstelle.
  2. Definieren Sie einen Typ, der das Schnittstellenverhalten erfüllt.
  3. Definieren Sie Methoden, die die Schnittstellenimplementierung erfüllen.

Zusammenfassend handelt es sich um "Schreibtypen, die Schnittstellen erfüllen". Dieser Code hat seinen eigenen Geruch , der folgende Gedanken suggeriert:

  • Nur ein Typ erfüllt die Schnittstelle, ohne die Absicht, sie weiter zu erweitern.
  • Funktionen verwenden normalerweise konkrete Typen anstelle von Schnittstellentypen.

Wie es stattdessen geht


Interfaces in Go fördern einen faulen Ansatz, was gut ist. Anstatt Typen zu schreiben, die Schnittstellen erfüllen, sollten Sie Schnittstellen schreiben, die den tatsächlichen praktischen Anforderungen entsprechen.

Was bedeutet: Anstatt Animal in der Tierverpackung zu definieren, definieren Sie es am Verwendungsort, dh im circus * -Paket.

 package animals type Dog struct{} func (a Dog) Speaks() string { return "woof" } 

 package circus type Speaker interface { Speaks() string } func Perform(a Speaker) string { return a.Speaks() } 

Ein natürlicherer Weg, dies zu tun, ist folgender:

  1. Typen definieren
  2. Definieren Sie die Schnittstelle am Verwendungsort.

Dieser Ansatz verringert die Abhängigkeit von den Komponenten der Tierverpackung. Das Reduzieren von Abhängigkeiten ist der richtige Weg, um fehlertolerante Software zu erstellen.

Bettgesetz


Es gibt ein gutes Prinzip für das Schreiben guter Software. Dies ist das Gesetz von Postel , das oft wie folgt formuliert wird:
"Seien Sie konservativ in Bezug auf das, was Sie meinen, und liberal in Bezug auf das, was Sie akzeptieren."
In Bezug auf Go lautet das Gesetz:

"Schnittstellen akzeptieren, Strukturen zurückgeben"

Alles in allem ist dies eine sehr gute Regel für das Entwerfen fehlertoleranter, stabiler Dinge * . Die Haupteinheit des Codes in Go ist eine Funktion. Beim Entwerfen von Funktionen und Methoden ist es hilfreich, das folgende Muster einzuhalten:

 func funcName(a INTERFACETYPE) CONCRETETYPE 

Hier akzeptieren wir alles, was eine Schnittstelle implementiert, die alles sein kann, einschließlich einer leeren. Ein Wert eines bestimmten Typs wird vorgeschlagen. Natürlich ist es sinnvoll zu begrenzen, was a sein kann. Wie ein Go-Sprichwort sagt:

"Die leere Oberfläche sagt nichts", sagte Rob Pike

Daher ist es sehr ratsam zu verhindern, dass Funktionen die interface{} akzeptieren.

Anwendungsbeispiel: Nachahmung


Ein eindrucksvolles Beispiel für die Vorteile der Anwendung des Postel-Gesetzes sind Testfälle. Angenommen, Sie haben eine Funktion, die folgendermaßen aussieht:

 func Takes(db Database) error 

Wenn die Database eine Schnittstelle ist, können Sie im Testcode einfach eine Nachahmung der Database bereitstellen, ohne ein echtes DB-Objekt übergeben zu müssen.

Wenn die Definition einer Schnittstelle im Voraus akzeptabel ist


Um ehrlich zu sein, ist das Programmieren eine ziemlich kostenlose Möglichkeit, Ideen auszudrücken. Es gibt keine unerschütterlichen Regeln. Natürlich können Sie Schnittstellen immer im Voraus definieren, ohne befürchten zu müssen, von der Code-Polizei festgenommen zu werden. Wenn Sie im Kontext vieler Pakete Ihre Funktionen kennen und beabsichtigen, eine bestimmte Schnittstelle innerhalb des Pakets zu akzeptieren, tun Sie dies.

Das Definieren einer Schnittstelle riecht normalerweise nach Überentwicklung, aber es gibt Situationen, in denen Sie offensichtlich genau das tun sollten. Insbesondere kommen folgende Beispiele in den Sinn:

  • Versiegelte Schnittstellen
  • Abstrakte Datentypen
  • Rekursive Schnittstellen

Als nächstes betrachten wir kurz jeden von ihnen.

Versiegelte Schnittstellen


Versiegelte Schnittstellen können nur im Zusammenhang mit mehreren Paketen diskutiert werden. Eine versiegelte Schnittstelle ist eine Schnittstelle mit nicht exportierten Methoden. Dies bedeutet, dass Benutzer außerhalb dieses Pakets keine Typen erstellen können, die diese Schnittstelle erfüllen. Dies ist nützlich, um einen Variantentyp zu emulieren, um ausführlich nach Typen zu suchen, die die Schnittstelle erfüllen.

Wenn Sie so etwas definiert haben:

 type Fooer interface { Foo() sealed() } 

Nur das von Fooer definierte Fooer kann es verwenden und daraus etwas Wertvolles schaffen. Auf diese Weise können Sie Brute-Force-Switch-Operatoren für Typen erstellen.

Über die versiegelte Schnittstelle können Analysewerkzeuge auch problemlos Übereinstimmungen mit Nichtkollisionsmustern erfassen. Das Sumtypes-Paket von BurntSushi soll dieses Problem lösen.

Abstrakte Datentypen


Ein weiterer Fall, bei dem eine Schnittstelle im Voraus definiert wird, besteht darin, abstrakte Datentypen zu erstellen. Sie können entweder versiegelt oder nicht versiegelt sein.

Ein gutes Beispiel hierfür ist das sort , das Teil der Standardbibliothek ist. Es definiert eine sortierbare Sammlung wie folgt

 type Interface interface { // Len —    . Len() int // Less      //   i     j. Less(i, j int) bool // Swap     i  j. Swap(i, j int) } 

Dieser Code hat viele Leute verärgert, denn wenn Sie das sort möchten, müssen Sie Methoden für die Schnittstelle implementieren. Vielen gefällt die Notwendigkeit, drei zusätzliche Codezeilen hinzuzufügen, nicht.

Ich finde jedoch, dass dies eine sehr elegante Form von Generika in Go ist. Seine Verwendung sollte häufiger gefördert werden.

Alternative und gleichzeitig elegante Designoptionen erfordern Typen höherer Ordnung. In diesem Beitrag werden wir sie nicht berücksichtigen.

Rekursive Schnittstellen


Dies ist wahrscheinlich ein weiteres Beispiel für Code mit einem Bestand, aber es gibt Zeiten, in denen es einfach unmöglich ist, die Verwendung zu vermeiden. Einfache Manipulationen ermöglichen es Ihnen, so etwas zu bekommen

 type Fooer interface { Foo() Fooer } 

Ein rekursives Schnittstellenmuster erfordert offensichtlich seine vorherige Definition. Die Empfehlung zur Definition der Point-of-Use-Schnittstelle ist hier nicht anwendbar.

Dieses Muster ist nützlich, um Kontexte mit anschließender Arbeit darin zu erstellen. Der vom Kontext geladene Code schließt sich normalerweise in das Paket ein und exportiert nur die Kontexte (ua das Tensor- Paket). In der Praxis begegne ich diesem Fall daher nicht so oft. Ich kann Ihnen noch etwas über Kontextmuster erzählen, aber lassen Sie es für einen anderen Beitrag.

Fazit


Trotz der Tatsache, dass in einer der Überschriften des Beitrags "Wie man es nicht macht" steht, versuche ich in keiner Weise, etwas zu verbieten. Vielmehr möchte ich die Leser dazu bringen, häufiger über Grenzbedingungen nachzudenken, da in solchen Fällen verschiedene Notsituationen auftreten.

Ich finde das Prinzip der Point-of-Use-Deklaration äußerst nützlich. Aufgrund seiner praktischen Anwendung stoße ich nicht auf Probleme, die entstehen, wenn ich sie vernachlässige.

Gelegentlich schreibe ich jedoch auch gelegentlich Schnittstellen im Java-Stil. Normalerweise passiert dies, wenn ich kurz zuvor viel Code in Java oder Python geschrieben habe. Der Wunsch, alles zu komplizieren und „in Form von Klassen darzustellen“, ist manchmal sehr groß, insbesondere wenn Sie Go-Code schreiben, nachdem Sie viel objektorientierten Code geschrieben haben.

Somit dient dieser Beitrag auch als Erinnerung an sich selbst, wie der Pfad zum Schreiben von Code aussieht, der anschließend keine Kopfschmerzen verursacht. Warten auf Ihre Kommentare!

Bild

Source: https://habr.com/ru/post/de411029/


All Articles