Zum Thema Designmuster wurden unzählige Artikel geschrieben und viele Bücher veröffentlicht. Dieses Thema hört jedoch nicht auf, relevant zu sein, da die Muster es uns ermöglichen, vorgefertigte, bewährte Lösungen zu verwenden, mit denen wir die Zeit für die Projektentwicklung verkürzen können, indem wir die Qualität des Codes verbessern und die technischen Schulden reduzieren.
Seit dem Aufkommen von Designmustern gibt es immer neue Beispiele für ihre effektive Verwendung. Und das ist wunderbar. Es gab jedoch eine Fliege in der Salbe: Jede Sprache hat ihre eigenen Besonderheiten. Und Golang - und noch mehr (es gibt nicht einmal ein klassisches OOP-Modell). Daher gibt es Variationen der Muster in Bezug auf einzelne Programmiersprachen. In diesem Artikel möchte ich auf das Thema Designmuster in Bezug auf Golang eingehen.
Dekorateur
Mit der Decorator-Vorlage können Sie zusätzliches Verhalten (statisch oder dynamisch) mit dem Objekt verbinden, ohne das Verhalten anderer Objekte derselben Klasse zu beeinflussen. Eine Vorlage wird häufig verwendet, um dem Prinzip der Einzelverantwortung zu entsprechen, da Sie damit Funktionen zwischen Klassen austauschen können, um bestimmte Probleme zu lösen.
Das bekannte DECORATOR-Muster ist in vielen Programmiersprachen weit verbreitet. In Golang basiert die gesamte Middleware auf ihrer Basis. Beispielsweise könnte die Abfrageprofilerstellung folgendermaßen aussehen:
func ProfileMiddleware(next http.Handler) http.Handler { started := time.Now() next.ServeHTTP() elapsed := time.Now().Sub(started) fmt.Printf("HTTP: elapsed time %d", elapsed) }
In diesem Fall ist die Dekorationsschnittstelle die einzige Funktion. Dies sollte in der Regel angestrebt werden. Ein Dekorateur mit einer breiteren Oberfläche kann jedoch manchmal nützlich sein. Betrachten Sie beispielsweise den Zugriff auf eine Datenbank (Paketdatenbank / SQL). Angenommen, wir müssen die gleiche Profilerstellung für Datenbankabfragen durchführen. In diesem Fall brauchen wir:
- Anstatt über einen Zeiger direkt mit der Datenbank zu interagieren, müssen wir über die Schnittstelle zur Interaktion übergehen (um das Verhalten von der Implementierung zu trennen).
- Erstellen Sie einen Wrapper für jede Methode, die eine SQL-Datenbankabfrage ausführt.
Als Ergebnis erhalten wir einen Dekorateur, mit dem Sie alle Abfragen in der Datenbank profilieren können. Die Vorteile dieses Ansatzes sind unbestreitbar:
- Bewahrt die Code-Sauberkeit der zentralen Datenbankzugriffskomponente.
- Jeder Dekorateur setzt eine einzelne Anforderung um. Dadurch wird eine einfache Implementierung erreicht.
- Aufgrund der Zusammensetzung der Dekorateure erhalten wir ein erweiterbares Modell, das sich leicht an unsere Bedürfnisse anpassen lässt.
- Durch das einfache Herunterfahren des Profilers erhalten wir im Produktionsmodus keinen Leistungsaufwand.
So können Sie beispielsweise die folgenden Arten von Dekorateuren implementieren:
- Herzschlag Pingen Sie eine Datenbank an, um eine Verbindung zu ihr aufrechtzuerhalten.
- Profiler. Die Ausgabe sowohl des Anforderungshauptteils als auch seiner Ausführungszeit.
- Schnüffler. Sammlung von Datenbankmetriken.
- Klon Klonen der ursprünglichen Datenbank für Debugging-Zwecke.
In der Regel ist bei der Implementierung von Rich Decorators nicht die Implementierung aller Methoden erforderlich: Es reicht aus, nicht implementierte Methoden an ein internes Objekt zu delegieren.
Angenommen, wir müssen einen erweiterten Logger implementieren, um DML-Abfragen für eine Datenbank zu verfolgen (um INSERT / UPDATE / DELETE-Abfragen zu verfolgen). In diesem Fall müssen wir nicht die gesamte Datenbankschnittstelle implementieren, sondern nur die Exec-Methode überlappen.
type MyDatabase interface{ Query(...) (sql.Rows, error) QueryRow(...) error Exec(query string, args ...interface) error Ping() error } type MyExecutor struct { MyDatabase } func (e *MyExecutor) Exec(query string, args ...interface) error { ... }
Wir sehen also, dass es nicht besonders schwierig ist, selbst einen reichen Dekorateur in der Golang-Sprache zu schaffen.
Vorlagenmethode
Template-Methode (Eng. Template-Methode) - Ein Verhaltensentwurfsmuster, das die Basis des Algorithmus definiert und es den Erben ermöglicht, einige Schritte des Algorithmus neu zu definieren, ohne seine Struktur als Ganzes zu ändern.
Die Golang-Sprache unterstützt das OOP-Paradigma, sodass diese Vorlage nicht in ihrer reinen Form implementiert werden kann. Nichts hindert uns jedoch daran, Konstruktoren mit geeigneten Funktionen zu improvisieren.
Angenommen, wir müssen eine Vorlagenmethode mit der folgenden Signatur definieren:
func Method(s string) error
Bei der Deklaration reicht es aus, ein Feld eines Funktionstyps zu verwenden. Um die Arbeit damit zu vereinfachen, können wir die Wrapper-Funktion verwenden, um den Aufruf mit dem fehlenden Parameter zu ergänzen und eine bestimmte Instanz, die entsprechende Konstruktorfunktion, zu erstellen.
type MyStruct struct { MethodImpl func (me *MyStruct, s string) error } // Wrapper for template method func (ms *MyStruct) Method(s string) error { return ms.MethodImpl(ms, s) }
Wie Sie dem Beispiel entnehmen können, unterscheidet sich die Semantik der Verwendung des Musters kaum von der klassischen OOP.
Adapter
Mit dem Entwurfsmuster „Adapter“ können Sie die Schnittstelle einer vorhandenen Klasse als andere Schnittstelle verwenden. Diese Vorlage wird häufig verwendet, um sicherzustellen, dass einige Klassen mit anderen zusammenarbeiten, ohne ihren Quellcode zu ändern.
Im Allgemeinen können Adapter als separate Funktionen und ganze Schnittstellen dienen. Wenn bei Schnittstellen alles mehr oder weniger klar und vorhersehbar ist, dann gibt es aus Sicht der einzelnen Funktionen Feinheiten.
Angenommen, wir schreiben einen Dienst mit einer internen API:
type MyService interface { Create(ctx context.Context, order int) (id int, err error) }
Wenn wir eine öffentliche API mit einer anderen Schnittstelle bereitstellen müssen (z. B. um mit gRPC zu arbeiten), können wir einfach die Adapterfunktionen verwenden, die sich mit der Konvertierung der Schnittstelle befassen. Zu diesem Zweck ist es sehr bequem, Verschlüsse zu verwenden.
type Endpoint func(ctx context.Context, request interface{}) (interface{}, error) type CreateRequest struct { Order int } type CreateResponse struct { ID int, Err error } func makeCreateEndpoint(s MyService) Endpoint { return func(ctx context.Context, request interface{}) (interface{}, error) {
Die Funktion makeCreateEndpoint besteht aus drei Standardschritten:
- Werte dekodieren
- Aufrufen einer Methode über die interne API des zu implementierenden Dienstes
- Wertekodierung
Alle Endpunkte im Gokit-Paket basieren auf diesem Prinzip.
Besucher
Die Vorlage "Besucher" ist eine Möglichkeit, den Algorithmus von der Struktur des Objekts zu trennen, in dem er arbeitet. Das Ergebnis der Trennung ist die Möglichkeit, vorhandenen Objektstrukturen neue Operationen hinzuzufügen, ohne sie zu ändern. Dies ist eine Möglichkeit, das Open / Closed-Prinzip einzuhalten.
Betrachten Sie das bekannte Besuchermuster am Beispiel geometrischer Formen.
type Geometry interface { Visit(GeometryVisitor) (interface{}, error) } type GeometryVisitor interface { VisitPoint(p *Point) (interface{}, error) VisitLine(l *Line) (interface{}, error) VisitCircle(c *Circle) (interface{}, error) } type Point struct{ X, Y float32 } func (point *Point) Visit(v GeometryVisitor) (interface{}, error) { return v.VisitPoint(point) } type Line struct{ X1, Y1 float32 X2, Y2 float32 } func (line *Line) Visit(v GeometryVisitor) (interface{}, error) { return v.VisitLine(line) } type Circle struct{ X, Y, R float32 } func (circle *Circle) Visit(v GeometryVisitor) (interface{}, error) { return v.VisitCircle(circle) }
Angenommen, wir möchten eine Strategie zur Berechnung des Abstands von einem bestimmten Punkt zu einer bestimmten Form schreiben.
type DistanceStrategy struct { X, Y float32 } func (s *DistanceStrategy) VisitPoint(p *Point) (interface{}, error) {
Ebenso können wir andere Strategien implementieren, die wir brauchen:
- Vertikale Ausdehnung
- Die horizontale Ausdehnung des Objekts
- Erstellen eines minimalen Spanning Square (MBR)
- Andere Primitive brauchen wir.
Darüber hinaus wissen zuvor definierte Zahlen (Punkt, Linie, Kreis ...) nichts über diese Strategien. Ihr einziges Wissen beschränkt sich auf die GeometryVisitor-Oberfläche. Auf diese Weise können Sie sie in einem separaten Paket isolieren.
Während ich an einem kartografischen Projekt arbeitete, hatte ich einmal die Aufgabe, eine Funktion zum Bestimmen der Entfernung zwischen zwei beliebigen geografischen Objekten zu schreiben. Die Lösungen waren sehr unterschiedlich, aber nicht effizient und elegant. In Anbetracht des Besuchermusters fiel mir auf, dass es zur Auswahl der Zielmethode dient und etwas an einen separaten Rekursionsschritt erinnert, der, wie Sie wissen, die Aufgabe vereinfacht. Dies veranlasste mich, Double Visitor zu verwenden. Stellen Sie sich meine Überraschung vor, als ich entdeckte, dass ein solcher Ansatz im Internet überhaupt nicht erwähnt wird.
type geometryStrategy struct{ G Geometry } func (s *geometryStrategy) VisitPoint(p *Point) (interface{}, error) { return sGVisit(&pointStrategy{Point: p}) } func (d *geometryStrategy) VisitLine(l *Line) (interface{}, error) { return sGVisit(&lineStrategy{Line: l}) } func (d *geometryStrategy) VisitCircle(c *Circle) (interface{}, error) { return sGVisit(&circleStrategy{Circle: c}) } type pointStrategy struct{ *Point } func (point *pointStrategy) Visit(p *Point) (interface{}, error) {
Daher haben wir einen zweistufigen Selektionsmechanismus entwickelt, der aufgrund seiner Arbeit die geeignete Methode zur Berechnung des Abstands zwischen zwei Grundelementen aufruft. Wir können nur diese Methoden schreiben und das Ziel ist erreicht. Auf diese Weise kann ein elegant nicht deterministisches Problem auf eine Reihe elementarer Funktionen reduziert werden.
Fazit
Trotz der Tatsache, dass es in Golang kein klassisches OOP gibt, erzeugt die Sprache einen eigenen Dialekt von Mustern, die auf den Stärken der Sprache spielen. Diese Muster gehen den Standardweg von der Verweigerung zur universellen Akzeptanz und werden im Laufe der Zeit zu Best Practices.
Wenn angesehene Habrozhiteli Gedanken zu Mustern haben, seien Sie bitte nicht schüchtern und äußern Sie Ihre Gedanken dazu.