Vor nicht allzu langer Zeit hat ein Kollege einen ausgezeichneten Beitrag
über die Verwendung von Go-Schnittstellen retweetet. Es werden einige Fehler bei der Verwendung der Schnittstellen in Go erläutert und einige Empfehlungen zur weiteren Verwendung dieser Schnittstellen gegeben.
In dem oben erwähnten Artikel zitiert der Autor die Schnittstelle aus dem
Sortierpaket der Standardbibliothek als Beispiel für einen abstrakten Datentyp. Es scheint mir jedoch, dass ein solches Beispiel die Idee bei realen Anwendungen nicht wirklich offenbart. Insbesondere bei Anwendungen, die die Logik eines Geschäftsfelds implementieren oder reale Probleme lösen.
Bei der Verwendung von Schnittstellen in Go wird häufig über Overengineering diskutiert. Und es kommt auch vor, dass Menschen nach dem Lesen dieser Art von Empfehlungen nicht nur aufhören, Schnittstellen zu missbrauchen, sondern versuchen, sie fast vollständig aufzugeben, wodurch sie sich der Verwendung eines der stärksten Programmierkonzepte im Prinzip (und einer der Stärken von Go in) entziehen insbesondere). Zum Thema typische Fehler in Go gibt es übrigens einen
guten Bericht von Stive Francia von Docker. Dort werden insbesondere Schnittstellen mehrfach erwähnt.
Im Allgemeinen stimme ich dem Autor des Artikels zu. Trotzdem schien es mir, dass das Thema der Verwendung von Schnittstellen als abstrakte Datentypen darin eher oberflächlich aufgedeckt wurde, daher möchte ich es ein wenig weiterentwickeln und mit Ihnen über dieses Thema nachdenken.
Beziehen Sie sich auf das Original
Zu Beginn des Artikels gibt der Autor ein kleines Codebeispiel an, mit dessen Hilfe er auf Fehler bei der Verwendung von Schnittstellen hinweist, die Entwickler häufig erstellen. Hier ist der Code.
package animal type Animal interface { Speaks() string }
package circus import "animal" func Perform(a animal.Animal) string { return a.Speaks() }
Der Autor nennt diesen Ansatz
"Java-artige Schnittstellenverwendung" . Wenn wir eine Schnittstelle deklarieren, implementieren wir den einzigen Typ und die einzigen Methoden, die diese Schnittstelle erfüllen. Ich stimme dem Autor zu, der Ansatz ist mittelmäßig. Der idiomatischere Code im Originalartikel lautet wie folgt:
package animal
package circus type Speaker interface { Speaks() string } func Perform(a Speaker) string { return a.Speaks() }
Hier ist im Allgemeinen alles klar und verständlich. Die Grundidee:
„Deklarieren Sie zuerst die Typen und erst dann die Schnittstellen am Verwendungsort .
“ Das ist richtig. Lassen Sie uns nun eine kleine Idee entwickeln, wie Sie Schnittstellen als abstrakte Datentypen verwenden können. Der Autor weist übrigens darauf hin, dass es in einer solchen Situation nichts Falsches gibt, die Schnittstelle
„im Voraus“ zu deklarieren. Wir werden mit dem gleichen Code arbeiten.
Spielen wir mit Abstraktionen
Wir haben also einen Zirkus und es gibt Tiere. Im Zirkus gibt es eine ziemlich abstrakte Methode
namens "Perform" , die die "
Speaker" -Schnittstelle verwendet und das Haustier Geräusche machen lässt. Zum Beispiel lässt er den Hund aus dem obigen Beispiel bellen. Erstellen Sie einen Tierbändiger. Da er hier nicht dumm ist, können wir ihn auch generell dazu bringen, Geräusche zu machen. Unsere Oberfläche ist ziemlich abstrakt. :) :)
package circus type Tamer struct{} func (t *Tamer) Speaks() string { return "WAT?" }
So weit, so gut. Wir gehen weiter. Lassen Sie uns unserem Zahmer beibringen, Haustieren Befehle zu erteilen. Bisher haben wir einen
Sprachbefehl . :) :)
package circus const ( ActVoice = iota ) func (t *Tamer) Command(action int, a Speaker) string { switch action { case ActVoice: return a.Speaks() } return "" }
package main import ( "animal" "circus" ) func main() { d := &animal.Dog{} t := &circus.Tamer{} t2 := &circus.Tamer{} t.Command(circus.ActVoice, d)
Mmmm, interessant ist es nicht? Es scheint, dass unser Kollege nicht glücklich ist, dass er in diesem Zusammenhang ein Haustier geworden ist? : D Was tun?
Sprecher scheint hier eine Abstraktion nicht sehr geeignet zu sein. Wir werden eine geeignetere erstellen (oder besser gesagt, wir werden auf irgendeine Weise die erste Version aus dem
„falschen Beispiel“ zurückgeben ), wonach wir die Methodennotation ändern werden.
package circus type Animal interface { Speaker } func (t *Tamer) Command(action int, a Animal) string { }
Das ändert nichts, Sie sagen, der Code wird trotzdem ausgeführt, weil Beide Schnittstellen implementieren eine Methode, und Sie haben im Allgemeinen Recht.
Dieses Beispiel zeigt jedoch eine wichtige Idee. Wenn wir über abstrakte Datentypen sprechen, ist der Kontext entscheidend. Zumindest die Einführung einer neuen Schnittstelle machte den Code um eine Größenordnung offensichtlicher und lesbarer.
Übrigens besteht eine der Möglichkeiten, den Zahmer zu zwingen, den Befehl
„Sprache“ nicht auszuführen, darin, einfach eine Methode hinzuzufügen, die er nicht haben sollte. Fügen wir eine solche Methode hinzu, die Auskunft darüber gibt, ob das Haustier trainierbar ist.
package circus type Animal interface { Speaker IsTrained() bool }
Jetzt kann der Zahmer nicht mehr anstelle eines Haustieres eingesetzt werden.
Verhalten erweitern
Wir werden unsere Haustiere zur Abwechslung zwingen, andere Befehle auszuführen. Außerdem fügen wir eine Katze hinzu.
package animal type Dog struct{} func (d Dog) IsTrained() bool { return true } func (d Dog) Speaks() string { return "woof" } func (d Dog) Jump() string { return "jumps" } func (d Dog) Sit() string { return "sit" } type Cat struct{} func (c Cat) IsTrained() bool { return false } func (c Cat) Speaks() string { return "meow!" } func (c Cat) Jump() string { return "meow!!" } func (c Cat) Sit() string { return "meow!!!" }
package circus const ( ActVoice = iota ActSit ActJump ) type Animal interface { Speaker IsTrained() bool Jump() string Sit() string } func (t *Tamer) Command(action int, a Animal) string { switch action { case ActVoice: return a.Speaks() case ActSit: return a.Sit() case ActJump: return a.Jump() } return "" }
Großartig, jetzt können wir unseren Tieren verschiedene Befehle geben, und sie werden sie ausführen. Bis zu dem einen oder anderen Grad ...: D.
package main import ( "animal" "circus" ) func main() { t := &circus.Tamer{} d := &animal.Dog{} t.Command(circus.ActVoice, d)
Unsere Hauskatzen sind für das Training nicht besonders geeignet. Deshalb werden wir dem Zahmer helfen und sicherstellen, dass er nicht mit ihnen leidet.
package circus func (t *Tamer) Command(action int, a Animal) string { if !a.IsTrained() { panic("Sorry but this animal doesn't understand your commands") }
Das ist besser Im Gegensatz zur ursprünglichen
Animal- Schnittstelle, die
Speaker dupliziert, haben wir jetzt die
"Animal" -Schnittstelle (die im Wesentlichen ein abstrakter Datentyp ist), die ein recht aussagekräftiges Verhalten implementiert.
Lassen Sie uns die Schnittstellengrößen diskutieren
Lassen Sie uns nun über ein Problem wie die Verwendung breiter Schnittstellen nachdenken.
Dies ist eine Situation, in der wir Schnittstellen mit einer Vielzahl von Methoden verwenden. In diesem Fall lautet die Empfehlung ungefähr so:
„Funktionen sollten Schnittstellen akzeptieren, die die von ihnen benötigten Methoden enthalten .
“Im Allgemeinen stimme ich zu, dass die Schnittstellen klein sein sollten, aber in diesem Fall ist der Kontext wieder wichtig. Kehren wir zu unserem Code zurück und bringen unserem Zahmer bei, sein Haustier zu
„loben“ .
Als Antwort auf das Lob wird das Haustier eine Stimme abgeben.
package circus func (t *Tamer) Praise(a Speaker) string { return a.Speaks() }
Es scheint, dass alles in Ordnung ist, wir verwenden die minimal notwendige Schnittstelle. Es ist nichts überflüssig. Aber auch hier das Problem. Verdammt, jetzt können wir
den anderen Trainer
"loben" und er
wird "eine Stimme geben" . : D Schnapp es dir? .. Der Kontext ist immer wichtig.
package main import ( "animal" "circus" ) func main() { t := &circus.Tamer{} t2 := &circus.Tamer{} d := &animal.Dog{} c := &animal.Cat{} t.Praise(d)
Warum bin ich In diesem Fall wäre die beste Lösung immer noch die Verwendung einer breiteren Schnittstelle (die den abstrakten Datentyp
"pet" darstellt). Da wir lernen wollen, wie man ein Haustier lobt, kann keine Kreatur Geräusche machen.
package circus
So viel besser. Wir können das Haustier loben, aber wir können den Zahmer nicht loben. Der Code wurde wieder einfacher und offensichtlicher.
Nun ein wenig zum Gesetz des Bettes
Der letzte Punkt, den ich ansprechen möchte, ist die Empfehlung, einen abstrakten Typ zu akzeptieren und eine bestimmte Struktur zurückzugeben. Im Originalartikel wird diese Erwähnung in dem Abschnitt gegeben, der das sogenannte
Postelsche Gesetz beschreibt .
Der Autor zitiert das Gesetz selbst :.
"Sei konservativ mit dem, was du tust, sei liberal mit dir, akzeptiere"
Und interpretiert es in Bezug auf die Go-Sprache
"Go": "Schnittstellen akzeptieren, Strukturen zurückgeben"
func funcName(a INTERFACETYPE) CONCRETETYPE
Sie stimmen im Allgemeinen zu, ich stimme zu, es ist eine gute Praxis. Ich möchte jedoch noch einmal betonen. Nimm es nicht wörtlich. Der Teufel steckt im Detail. Wie immer ist der Kontext wichtig.
Nicht immer sollte eine Funktion einen bestimmten Typ zurückgeben. Das heißt, Wenn Sie einen abstrakten Typ benötigen, geben Sie ihn zurück. Sie müssen nicht versuchen, Code neu zu schreiben, ohne die Abstraktion zu vermeiden.
Hier ist ein kleines Beispiel. Ein Elefant erschien in einem nahe gelegenen
„afrikanischen“ Zirkus, und Sie haben die Zirkusbesitzer gebeten, einen Elefanten für eine neue Show zu leihen. In diesem Fall ist es für Sie nur wichtig, dass der Elefant dieselben Befehle wie andere Haustiere ausführen kann. Die Größe eines Elefanten oder das Vorhandensein eines Stammes spielt in diesem Zusammenhang keine Rolle.
package african import "circus" type Elephant struct{} func (e Elephant) Speaks() string { return "pawoo!" } func (e Elephant) Jump() string { return "o_O" } func (e Elephant) Sit() string { return "sit" } func (e Elephant) IsTrained() bool { return true } func GetElephant() circus.Animal { return &Elephant{} }
package main import ( "african" "circus" ) func main() { t := &circus.Tamer{} e := african.GetElephant() t.Command(circus.ActVoice, e)
Wie Sie sehen können, verwenden wir möglicherweise die Abstraktion, da wir uns nicht um die spezifischen Parameter eines Elefanten kümmern, die ihn von anderen Haustieren unterscheiden. In diesem Fall ist es durchaus angebracht, die Schnittstelle zurückzugeben.
Zusammenfassend
Der Kontext ist entscheidend, wenn es um Abstraktionen geht. Vernachlässigen Sie Abstraktionen nicht und haben Sie Angst vor ihnen, so wie Sie sie nicht missbrauchen sollten. Sie sollten Empfehlungen nicht als Regeln nehmen. Es gibt Ansätze, die im Laufe der Zeit getestet wurden, es gibt Ansätze, die noch getestet werden müssen. Ich hoffe, ich konnte das Thema der Verwendung von Schnittstellen als abstrakte Datentypen etwas vertiefen und mich von den üblichen Beispielen aus der Standardbibliothek entfernen.
Natürlich mag dieser Beitrag für manche Menschen zu offensichtlich erscheinen, und Beispiele werden aus dem Finger gesaugt. Für andere mögen meine Gedanken kontrovers und die Argumente nicht überzeugend sein. Trotzdem kann jemand inspiriert sein und anfangen, nicht nur über den Code, sondern auch über das Wesentliche der Dinge sowie über Abstraktionen im Allgemeinen ein wenig tiefer nachzudenken.
Die Hauptsache, Freunde, ist, dass Sie sich ständig weiterentwickeln und echte Freude an der Arbeit haben. Gut zu allen!
PS. Beispielcode und die endgültige Version finden Sie
auf GitHub .