Mauvais conseils pour un programmeur Go
Dans la première partie de la publication, j'ai expliqué comment devenir un programmeur Go "malveillant". Le mal se présente sous plusieurs formes, mais dans la programmation, il réside dans la difficulté délibérée de comprendre et de maintenir le code. Les mauvais programmes ignorent les moyens de base du langage au profit de techniques qui offrent des avantages à court terme en échange de problèmes à long terme. Pour rappel, les «mauvaises pratiques» de Go comprennent:
- Forfaits mal nommés et organisés
- Interfaces mal organisées
- Passer des pointeurs vers des variables dans des fonctions pour remplir leurs valeurs
- Utiliser la panique au lieu des erreurs
- Utilisation des fonctions init et des importations vides pour configurer les dépendances
- Téléchargez les fichiers de configuration à l'aide des fonctions init
- Utilisation de frameworks au lieu de bibliothèques
Grosse boule de mal
Que se passe-t-il si nous rassemblons toutes nos mauvaises pratiques? Nous aurions un cadre qui utiliserait de nombreux fichiers de configuration, remplirait les champs de structure à l'aide de pointeurs, définirait des interfaces pour décrire les types publiés, s'appuierait sur du code «magique» et paniquerait en cas de problème.
Et je l'ai fait. Si vous allez sur
https://github.com/evil-go , vous verrez
Fall , un framework DI qui vous permet de mettre en œuvre toutes les pratiques «mauvaises» que vous souhaitez. J'ai soudé Fall avec un tout petit framework web Outboy qui suit les mêmes principes.
Vous vous demandez peut-être à quel point ils sont méchants? Voyons voir. Je suggère d'aller pour un simple programme Go (écrit en utilisant les meilleures pratiques) qui fournit un point de terminaison http. Et puis réécrivez-le en utilisant Fall et Outboy.
Meilleures pratiques
Notre programme se trouve dans un seul paquet appelé Greet, qui utilise toutes les fonctions de base pour implémenter notre point de terminaison. Comme il s'agit d'un exemple, nous utilisons un DAO de travail en mémoire, avec trois champs pour les valeurs que nous retournerons. Nous aurons également une méthode qui, selon l'entrée, remplace l'appel à notre base de données et renvoie le message d'accueil souhaité.
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 } }
Vient ensuite la logique métier. Pour l'implémenter, nous définissons une structure pour stocker les données de sortie, une interface GreetingFinder pour décrire ce que la logique métier recherche au niveau de la recherche de données, et une structure pour stocker la logique métier elle-même avec un champ pour GreetingFinder. La logique réelle est simple - elle appelle simplement GreetingFinder et gère toutes les erreurs qui pourraient se produire.
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 }
Vient ensuite la couche Web, et pour cette partie, nous définissons l'interface Greeter, qui fournit toute la logique métier dont nous avons besoin, ainsi que la structure contenant le gestionnaire http configuré à l'aide de Greeter. Ensuite, nous créons une méthode pour implémenter l'interface http.Handler, qui divise la demande http, appelle greeter-a (message d'accueil), traite les erreurs et renvoie les résultats.
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)) }
Ceci est la fin du paquet d'accueil. Ensuite, nous verrons comment un «bon» développeur Go écrira main pour utiliser ce package. Dans le package principal, nous définissons une structure appelée Config, qui contient les propriétés que nous devons exécuter. La fonction principale fait alors 3 choses.
- Tout d'abord, il appelle la fonction loadProperties, qui utilise une bibliothèque simple ( https://github.com/evil-go/good-sample/blob/master/config/config.go ) pour charger les propriétés du fichier de configuration et les place dans notre copie d'une config. Si le téléchargement de la configuration a échoué, la fonction principale signale une erreur et se ferme.
- Deuxièmement, la fonction principale lie les composants dans le package d'accueil, en leur attribuant explicitement des valeurs à partir de la configuration et en configurant les dépendances.
- Troisièmement, il appelle une petite bibliothèque de serveur ( https://github.com/evil-go/good-sample/blob/master/server/server.go ) et transmet l'adresse, la méthode HTTP et http.Handler au point de terminaison pour traitement des demandes. Un appel de bibliothèque lance un service Web. Et ceci est toute notre application.
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) } }
L'exemple est assez court, mais il montre à quel point le Go cool est écrit; certaines choses sont ambiguës, mais en général, il est clair ce qui se passe. Nous collons de petites bibliothèques spécialement conçues pour fonctionner ensemble. Rien n'est caché; n'importe qui peut prendre ce code, comprendre comment ses parties sont connectées entre elles et, si nécessaire, les refaire à de nouvelles.
Tache noire
Nous allons maintenant considérer la version de Fall et Outboy. La première chose que nous ferons est de diviser le package de bienvenue en plusieurs packages, chacun contenant une couche d'application. Voici le package DAO. Il importe Fall, notre framework DI, et puisque nous sommes «diaboliques» et définissons au contraire les relations avec les interfaces, nous allons définir une interface appelée GreetDao. Veuillez noter - nous avons supprimé tous les liens vers les erreurs; si quelque chose ne va pas, on panique. À ce stade, nous avons déjà un mauvais packaging, de mauvaises interfaces et de mauvais bugs. Bon début!
Nous avons légèrement renommé notre structure à partir d'un bon exemple. Les champs ont maintenant des balises struct; ils sont utilisés pour que Fall définisse la valeur enregistrée dans le champ. Nous avons également une fonction init pour notre package, avec laquelle nous accumulons la «puissance du mal». Dans la fonction package init, nous appelons Fall deux fois:
- Une fois pour enregistrer un fichier de configuration qui fournit des valeurs pour les balises de structure.
- Et un autre, pour enregistrer un pointeur sur une instance de la structure. Fall sera en mesure de remplir ces champs pour nous et de rendre le DAO disponible pour une utilisation par un autre code.
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{}) }
Voyons le paquet de services. Il importe le package DAO car il a besoin d'accéder à l'interface qui y est définie. Le package de services importe également le package de modèle, que nous n'avons pas encore pris en compte - nous y stockons nos types de données. Et nous importons Fall, car, comme tous les "bons" frameworks, il pénètre partout. Nous définissons également une interface de service pour donner accès à la couche web. Encore une fois, sans gestion d'erreur.
La mise en œuvre de notre service dispose désormais d'une balise structurelle avec fil. Le fil marqué sur le terrain connecte automatiquement sa dépendance lorsque la structure est enregistrée à l'automne. Dans notre petit exemple, il est clair ce qui sera attribué à ce champ. Mais dans un programme plus vaste, vous saurez seulement que quelque part cette interface GreetDao est implémentée et enregistrée à l'automne. Vous ne pouvez pas contrôler le comportement de dépendance.
Vient ensuite la méthode de notre service, qui a été légèrement modifiée pour obtenir la structure GreetResponse du package de modèle, et qui supprime toute gestion d'erreur. Enfin, nous avons une fonction init dans le package qui enregistre une instance de service à l'automne.
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{}) }
Voyons maintenant le package du modèle. Il n'y a surtout rien à regarder. On peut voir que le modèle est séparé du code qui le crée, seulement pour diviser le code en couches.
package model type GreetResponse struct { Message string }
Dans le package Web, nous avons une interface Web. Ici, nous importons Fall et Outboy, et nous importons également le package de services dont dépend le package Web. Parce que les frameworks ne fonctionnent bien ensemble que lorsqu'ils s'intègrent dans les coulisses, Fall a un code spécial pour s'assurer qu'il fonctionne ensemble et Outboy. Nous modifions également la structure pour qu'elle devienne le contrôleur de notre application Web. Elle a deux domaines:
- Le premier est connecté via Fall à l'implémentation de l'interface GreetService à partir du package de services.
- Le second est le chemin d'accès à notre seul point de terminaison Web. On lui attribue la valeur du fichier de configuration enregistré dans la fonction init de ce package.
Notre gestionnaire http a été renommé GetHello et il est désormais exempt de gestion des erreurs. Nous avons également la méthode Init (avec une majuscule), qui ne doit pas être confondue avec la fonction init. Init est une méthode magique qui est appelée pour les structures enregistrées en automne après avoir rempli tous les champs. Dans Init, nous appelons Outboy pour enregistrer notre contrôleur et son point de terminaison dans le chemin défini à l'aide de Fall. En regardant le code, vous verrez le chemin et le gestionnaire, mais la méthode HTTP n'est pas spécifiée. Dans Outboy, le nom de la méthode est utilisé pour déterminer à quelle méthode HTTP le gestionnaire répond. Puisque notre méthode s'appelle GetHello, elle répond aux requêtes GET. Si vous ne connaissez pas ces règles, vous ne pourrez pas comprendre à quelles demandes il répond. C'est vrai, c'est très méchant?
Enfin, nous appelons la fonction init pour enregistrer le fichier de configuration et le contrôleur à l'automne.
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{}) }
Il ne reste plus qu'à montrer comment nous exécutons le programme. Dans le package principal, nous utilisons des importations vides pour enregistrer Outboy et le package Web. Et la fonction principale appelle fall.Start () pour lancer l'application entière.
package main import ( _ "github.com/evil-go/evil-sample/web" "github.com/evil-go/fall" _ "github.com/evil-go/outboy" ) func main() { fall.Start() }
Perturbation du tégument
Et le voici, un programme complet écrit en utilisant tous nos outils Go diaboliques. C'est un cauchemar. Elle cache comme par magie la façon dont certaines parties du programme s'emboîtent et rend la compréhension de son travail terriblement difficile.
Et pourtant, vous devez admettre qu'il y a quelque chose d'attrayant dans l'écriture de code avec Fall et Outboy. Pour un petit programme, on pourrait même dire que c'est une amélioration. Voyez comme il est facile de configurer! Je peux connecter des dépendances avec presque aucun code! J'ai enregistré un gestionnaire pour la méthode, en utilisant simplement son nom! Et sans aucune manipulation d'erreur, tout semble si propre!
C'est ainsi que le mal fonctionne. À première vue, c'est vraiment attrayant. Mais à mesure que votre programme change et se développe, toute cette magie commence seulement à interférer, compliquant la compréhension de ce qui se passe. Ce n'est que lorsque vous êtes complètement obsédé par le mal que vous regardez en arrière et réalisez que vous êtes pris au piège.
Pour les développeurs Java, cela peut sembler familier. Ces techniques peuvent être trouvées dans de nombreux frameworks Java populaires. Comme je l'ai mentionné plus tôt, je travaille avec Java depuis plus de 20 ans, à partir de 1.0.2 en 1996. Dans de nombreux cas, les développeurs Java ont été les premiers à rencontrer des problèmes d'écriture de logiciels d'entreprise à grande échelle à l'ère d'Internet. Je me souviens de l'époque où les servlets, EJB, Spring et Hibernate sont apparus. Les décisions prises par les développeurs Java à ce moment-là avaient du sens. Mais au fil des ans, ces techniques montrent leur âge. Les langages plus récents, tels que Go, sont conçus pour éliminer les points faibles rencontrés lors de l'utilisation de techniques plus anciennes. Cependant, lorsque les développeurs Java commencent à apprendre Go et à écrire du code avec eux, ils doivent se rappeler qu'essayer de reproduire des modèles à partir de Java produira de mauvais résultats.
Go a été conçu pour une programmation sérieuse - pour des projets qui couvrent des centaines de développeurs et des dizaines d'équipes. Mais pour que Go puisse le faire, vous devez l'utiliser comme il fonctionne le mieux. Nous pouvons choisir d'être mauvais ou bons. Si nous choisissons le mal, nous pouvons encourager les jeunes développeurs Go à changer leur style et leurs techniques avant de comprendre Go. Ou nous pouvons choisir le bien. Une partie de notre travail en tant que développeurs Go est d'éduquer les jeunes Gophers (Gophers), pour les aider à comprendre les principes qui sous-tendent nos meilleures pratiques.
Le seul inconvénient de suivre la voie du bien est que vous devez chercher une autre façon d'exprimer votre mal intérieur.
Peut-être essayez-vous de rouler à une vitesse de 30 km / h sur la route fédérale?