Gutes tun, Schlechtes tun: Mit Go bösen Code schreiben, Teil 2

Schlechte Tipps für einen Go-Programmierer

Bild

Im ersten Teil der Veröffentlichung erklärte ich, wie man ein "bösartiger" Go-Programmierer wird. Das Böse gibt es in vielen Formen, aber bei der Programmierung liegt es in der absichtlichen Schwierigkeit, den Code zu verstehen und zu pflegen. Böse Programme ignorieren die grundlegenden Sprachmittel zugunsten von Techniken, die kurzfristige Vorteile im Austausch für langfristige Probleme bieten. Zur kurzen Erinnerung gehören zu Go's bösen "Praktiken":

  • Schlecht benannte und organisierte Pakete
  • Falsch organisierte Schnittstellen
  • Übergeben von Zeigern auf Variablen in Funktionen, um deren Werte zu füllen
  • Panik statt Fehler
  • Verwenden von Init-Funktionen und leeren Importen zum Konfigurieren von Abhängigkeiten
  • Laden Sie Konfigurationsdateien mit den Init-Funktionen herunter
  • Verwenden von Frameworks anstelle von Bibliotheken

Großer Ball des Bösen


Was passiert, wenn wir alle unsere bösen Praktiken zusammenfügen? Wir hätten ein Framework, das viele Konfigurationsdateien verwendet, die Strukturfelder mit Zeigern ausfüllt, Schnittstellen zur Beschreibung veröffentlichter Typen definiert, sich auf „magischen“ Code verlässt und bei jedem Problem in Panik gerät.

Und ich habe es geschafft. Wenn Sie zu https://github.com/evil-go gehen, sehen Sie Fall , ein DI-Framework, mit dem Sie alle gewünschten „bösen“ Praktiken implementieren können. Ich habe Fall mit einem winzigen Outboy-Webframework gelötet, das denselben Prinzipien folgt.

Sie fragen sich vielleicht, wie bösartig sie sind? Mal sehen. Ich empfehle ein einfaches Go-Programm (geschrieben mit Best Practices), das den http-Endpunkt bereitstellt. Und dann schreiben Sie es mit Fall und Outboy neu.

Best Practices


Unser Programm besteht aus einem einzigen Paket namens greet, das alle Grundfunktionen zur Implementierung unseres Endpunkts verwendet. Da dies ein Beispiel ist, verwenden wir ein im Speicher arbeitendes DAO mit drei Feldern für die Werte, die wir zurückgeben werden. Wir werden auch eine Methode haben, die abhängig von der Eingabe den Aufruf unserer Datenbank ersetzt und die gewünschte Begrüßung zurückgibt.

package greet type Dao struct { DefaultMessage string BobMessage string JuliaMessage string } func (sdi Dao) GreetingForName(name string) (string, error) { switch name { case "Bob": return sdi.BobMessage, nil case "Julia": return sdi.JuliaMessage, nil default: return sdi.DefaultMessage, nil } } 

Als nächstes kommt die Geschäftslogik. Um dies zu implementieren, definieren wir eine Struktur zum Speichern von Ausgabedaten, eine GreetingFinder-Schnittstelle zum Beschreiben, wonach Geschäftslogik auf der Datensuchebene sucht, und eine Struktur zum Speichern von Geschäftslogik selbst mit einem Feld für GreetingFinder. Die eigentliche Logik ist einfach: Sie ruft lediglich GreetingFinder auf und behandelt eventuell auftretende Fehler.

 type Response struct { Message string } type GreetingFinder interface { GreetingForName(name string) (string, error) } type Service struct { GreetingFinder GreetingFinder } func (ssi Service) Greeting(name string)(Response, error) { msg, err := ssi.GreetingFinder.GreetingForName(name) if err != nil { return Response{}, err } return Response{Message: msg}, nil } 

Dann kommt die Webebene, und für diesen Teil definieren wir die Greeter-Schnittstelle, die die gesamte Geschäftslogik enthält, die wir benötigen, sowie die Struktur, die den mit Greeter konfigurierten http-Handler enthält. Anschließend erstellen wir eine Methode zum Implementieren der http.Handler-Schnittstelle, die die http-Anforderung aufteilt, greeter-a (Begrüßung) aufruft, Fehler verarbeitet und die Ergebnisse zurückgibt.

 type Greeter interface { Greeting(name string) (Response, error) } type Controller struct { Greeter Greeter } func (mc Controller) ServeHTTP(rw http.ResponseWriter, req *http.Request) { result, err := mc.Greeter.Greeting( req.URL.Query().Get("name")) if err != nil { rw.WriteHeader(http.StatusInternalServerError) rw.Write([]byte(err.Error())) return } rw.Write([]byte(result.Message)) } 

Dies ist das Ende des Grußpakets. Als nächstes werden wir sehen, wie ein "guter" Go-Entwickler main schreiben würde, um dieses Paket zu verwenden. Im Hauptpaket definieren wir eine Struktur namens Config, die die Eigenschaften enthält, die wir ausführen müssen. Die Hauptfunktion macht dann 3 Dinge.

  • Zunächst wird die Funktion loadProperties aufgerufen, die eine einfache Bibliothek ( https://github.com/evil-go/good-sample/blob/master/config/config.go ) verwendet, um die Eigenschaften aus der Konfigurationsdatei zu laden und zu platzieren in unserer Kopie einer Konfiguration. Wenn der Konfigurationsdownload fehlgeschlagen ist, meldet die Hauptfunktion einen Fehler und wird beendet.
  • Zweitens bindet die Hauptfunktion die Komponenten im Greet-Paket, weist ihnen explizit Werte aus der Konfiguration zu und richtet die Abhängigkeiten ein.
  • Drittens ruft es eine kleine Serverbibliothek ( https://github.com/evil-go/good-sample/blob/master/server/server.go ) auf und übergibt die Adresse, die HTTP-Methode und http.Handler an den Endpunkt für Anforderungsverarbeitung. Ein Bibliotheksaufruf startet einen Webdienst. Und das ist unsere gesamte Anwendung.

 package main type Config struct { DefaultMessage string BobMessage string JuliaMessage string Path string } func main() { c, err := loadProperties() if err != nil { fmt.Println(err) os.Exit(1) } dao := greet.Dao{ DefaultMessage: c.DefaultMessage, BobMessage: c.BobMessage, JuliaMessage: c.JuliaMessage, } svc := greet.Service{GreetingFinder: dao} controller := greet.Controller{Greeter: svc} err = server.Start(server.Endpoint{c.Path, http.MethodGet, controller}) if err != nil { fmt.Println(err) os.Exit(1) } } 

Das Beispiel ist ziemlich kurz, aber es zeigt, wie cool Go geschrieben ist. Einige Dinge sind nicht eindeutig, aber im Allgemeinen ist klar, was passiert. Wir kleben kleine Bibliotheken, die speziell für die Zusammenarbeit eingerichtet wurden. Nichts ist verborgen; Jeder kann diesen Code nehmen, verstehen, wie seine Teile miteinander verbunden sind, und sie gegebenenfalls zu neuen wiederholen.

Schwarzer Fleck


Jetzt werden wir die Version von Fall und Outboy betrachten. Als erstes teilen wir das Greet-Paket in mehrere Pakete auf, von denen jedes eine Anwendungsschicht enthält. Hier ist das DAO-Paket. Es importiert Fall, unser DI-Framework, und da wir "böse" sind und im Gegenteil Beziehungen zu Schnittstellen definieren, werden wir eine Schnittstelle namens GreetDao definieren. Bitte beachten Sie, dass wir alle Links zu Fehlern entfernt haben. Wenn etwas nicht stimmt, geraten wir in Panik. Zu diesem Zeitpunkt haben wir bereits schlechte Verpackungen, schlechte Schnittstellen und schlechte Fehler. Toller Start!

Wir haben unsere Struktur anhand eines guten Beispiels leicht umbenannt. Felder haben jetzt Struktur-Tags. Sie werden verwendet, um Fall dazu zu bringen, den registrierten Wert im Feld festzulegen. Wir haben auch eine Init-Funktion für unser Paket, mit der wir „böse Kraft“ ansammeln. In der Paketinitialfunktion rufen wir Fall zweimal auf:

  • Einmal, um eine Konfigurationsdatei zu registrieren, die Werte für Struktur-Tags bereitstellt.
  • Und eine andere, um einen Zeiger auf eine Instanz der Struktur zu registrieren. Fall kann diese Felder für uns ausfüllen und das DAO für andere Codes zur Verfügung stellen.

 package dao import ( "github.com/evil-go/fall" ) type GreetDao interface { GreetingForName(name string) string } type greetDaoImpl struct { DefaultMessage string `value:"message.default"` BobMessage string `value:"message.bob"` JuliaMessage string `value:"message.julia"` } func (gdi greetDaoImpl) GreetingForName(name string) string { switch name { case "Bob": return gdi.BobMessage case "Julia": return gdi.JuliaMessage default: return gdi.DefaultMessage } } func init() { fall.RegisterPropertiesFile("dao.properties") fall.Register(&greetDaoImpl{}) } 

Sehen wir uns das Servicepaket an. Es importiert das DAO-Paket, da es Zugriff auf die dort definierte Schnittstelle benötigt. Das Servicepaket importiert auch das Modellpaket, das wir noch nicht berücksichtigt haben - wir speichern dort unsere Datentypen. Und wir importieren den Herbst, weil er wie alle "guten" Frameworks überall durchdringt. Wir definieren auch eine Schnittstelle für den Service, um den Zugriff auf die Webebene zu ermöglichen. Wieder ohne Fehlerbehandlung.

Die Implementierung unseres Service hat jetzt ein strukturelles Tag mit Draht. Der feldmarkierte Draht verbindet automatisch seine Abhängigkeit, wenn die Struktur im Herbst registriert wird. In unserem winzigen Beispiel ist klar, was diesem Feld zugewiesen wird. In einem größeren Programm wissen Sie jedoch nur, dass diese GreetDao-Schnittstelle irgendwo implementiert und im Herbst registriert ist. Sie können das Abhängigkeitsverhalten nicht steuern.

Als nächstes folgt die Methode unseres Dienstes, die geringfügig geändert wurde, um die GreetResponse-Struktur aus dem Modellpaket zu erhalten, und die jegliche Fehlerbehandlung beseitigt. Schließlich haben wir eine Init-Funktion im Paket, die eine Dienstinstanz im Herbst registriert.

 package service import ( "github.com/evil-go/fall" "github.com/evil-go/evil-sample/dao" "github.com/evil-go/evil-sample/model" ) type GreetService interface { Greeting(string) model.GreetResponse } type greetServiceImpl struct { Dao dao.GreetDao `wire:""` } func (ssi greetServiceImpl) Greeting(name string) model.GreetResponse { return model.GreetResponse{Message: ssi.Dao.GreetingForName(name)} } func init() { fall.Register(&greetServiceImpl{}) } 

Schauen wir uns nun das Modellpaket an. Es gibt vor allem nichts zu sehen. Es ist ersichtlich, dass das Modell von dem Code, der es erstellt, getrennt wird, nur um den Code in Ebenen zu unterteilen.

 package model type GreetResponse struct { Message string } 

Im Webpaket haben wir eine Webschnittstelle. Hier importieren wir sowohl Fall als auch Outboy und importieren auch das Servicepaket, von dem das Webpaket abhängt. Da Frameworks nur dann gut zusammenarbeiten, wenn sie hinter den Kulissen integriert sind, verfügt Fall über einen speziellen Code, um sicherzustellen, dass es und Outboy zusammenarbeiten. Wir ändern auch die Struktur, damit sie zum Controller für unsere Webanwendung wird. Sie hat zwei Felder:

  • Die erste ist über Fall mit der Implementierung der GreetService-Schnittstelle aus dem Servicepaket verbunden.
  • Der zweite ist der Pfad für unseren einzigen Webendpunkt. Es wird der Wert aus der Konfigurationsdatei zugewiesen, die in der Init-Funktion dieses Pakets registriert ist.

Unser http-Handler wurde in GetHello umbenannt und ist jetzt frei von Fehlerbehandlung. Wir haben auch die Init-Methode (mit einem Großbuchstaben), die nicht mit der Init-Funktion verwechselt werden sollte. Init ist eine magische Methode, die für Strukturen aufgerufen wird, die im Herbst registriert wurden, nachdem alle Felder ausgefüllt wurden. In Init rufen wir Outboy auf, um unseren Controller und seinen Endpunkt in dem Pfad zu registrieren, der mit Fall festgelegt wurde. Wenn Sie sich den Code ansehen, sehen Sie den Pfad und den Handler, aber die HTTP-Methode ist nicht angegeben. In Outboy wird anhand des Methodennamens bestimmt, auf welche HTTP-Methode der Handler reagiert. Da unsere Methode GetHello heißt, reagiert sie auf GET-Anfragen. Wenn Sie diese Regeln nicht kennen, können Sie nicht verstehen, welche Anfragen er beantwortet. Stimmt, das ist sehr bösartig?

Schließlich rufen wir die Init-Funktion auf, um die Konfigurationsdatei und den Controller im Herbst zu registrieren.

 package web import ( "github.com/evil-go/fall" "github.com/evil-go/outboy" "github.com/evil-go/evil-sample/service" "net/http" ) type GreetController struct { Service service.GreetService `wire:""` Path string `value:"controller.path.hello"` } func (mc GreetController) GetHello(rw http.ResponseWriter, req *http.Request) { result := mc.Service.Greeting(req.URL.Query().Get("name")) rw.Write([]byte(result.Message)) } func (mc GreetController) Init() { outboy.Register(mc, map[string]string{ "GetHello": mc.Path, }) } func init() { fall.RegisterPropertiesFile("web.properties") fall.Register(&GreetController{}) } 

Es bleibt nur zu zeigen, wie wir das Programm ausführen. Im Hauptpaket verwenden wir leere Importe, um Outboy und das Webpaket zu registrieren. Und die Hauptfunktion ruft fall.Start () auf, um die gesamte Anwendung zu starten.

 package main import ( _ "github.com/evil-go/evil-sample/web" "github.com/evil-go/fall" _ "github.com/evil-go/outboy" ) func main() { fall.Start() } 

Störung des Integuments


Und hier ist es, ein komplettes Programm, das mit all unseren bösen Go-Werkzeugen geschrieben wurde. Das ist ein Albtraum. Sie verbirgt auf magische Weise, wie Teile des Programms zusammenpassen, und macht es fürchterlich schwierig, ihre Arbeit zu verstehen.

Und doch müssen Sie zugeben, dass das Schreiben von Code mit Fall und Outboy etwas Attraktives hat. Für ein winziges Programm könnte man sogar sagen, dass dies eine Verbesserung ist. Sehen Sie, wie einfach die Konfiguration ist! Ich kann Abhängigkeiten fast ohne Code verbinden! Ich habe einen Handler für die Methode registriert, nur mit ihrem Namen! Und ohne Fehlerbehandlung sieht alles so sauber aus!

So funktioniert das Böse. Auf den ersten Blick ist es wirklich attraktiv. Aber wenn sich Ihr Programm ändert und wächst, beginnt all diese Magie nur zu stören, was das Verständnis dessen, was passiert, erschwert. Nur wenn Sie völlig vom Bösen besessen sind, blicken Sie zurück und stellen fest, dass Sie gefangen sind.

Für Java-Entwickler mag dies vertraut erscheinen. Diese Techniken finden sich in vielen gängigen Java-Frameworks. Wie bereits erwähnt, arbeite ich seit über 20 Jahren mit Java, beginnend mit 1.0.2 im Jahr 1996. In vielen Fällen waren Java-Entwickler die ersten, die im Internetzeitalter Probleme beim Schreiben umfangreicher Unternehmenssoftware hatten. Ich erinnere mich an die Zeiten, als Servlets, EJB, Spring und Hibernate gerade erschienen sind. Die Entscheidungen, die Java-Entwickler damals getroffen haben, waren sinnvoll. Aber im Laufe der Jahre zeigen diese Techniken ihr Alter. Neuere Sprachen wie Go sollen die bei älteren Techniken auftretenden Schwachstellen beseitigen. Wenn Java-Entwickler jedoch anfangen, Go zu lernen und Code damit zu schreiben, sollten sie sich daran erinnern, dass der Versuch, Muster aus Java zu reproduzieren, zu schlechten Ergebnissen führt.

Go wurde für seriöse Programmierung entwickelt - für Projekte, die Hunderte von Entwicklern und Dutzende von Teams umfassen. Damit Go dies tun kann, müssen Sie es so verwenden, wie es am besten funktioniert. Wir können wählen, ob wir böse oder gut sind. Wenn wir uns für das Böse entscheiden, können wir junge Go-Entwickler ermutigen, ihren Stil und ihre Techniken zu ändern, bevor sie Go verstehen. Oder wir können gut wählen. Ein Teil unserer Arbeit als Go-Entwickler besteht darin, junge Gophers (Gophers) auszubilden, um ihnen zu helfen, die Prinzipien zu verstehen, die unseren Best Practices zugrunde liegen.

Der einzige Nachteil, wenn man dem Weg des Guten folgt, ist, dass man nach einem anderen Weg suchen muss, um sein inneres Übel auszudrücken. Versuchen Sie vielleicht, auf der Bundesstraße mit einer Geschwindigkeit von 30 km / h zu fahren?

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


All Articles