Fazendo o bem, fazendo o mal: escrevendo código mal com Go, parte 2

Dicas ruins para um programador Go

imagem

Na primeira parte da publicação, expliquei como se tornar um programador Go "malicioso". O mal vem de várias formas, mas na programação está na dificuldade deliberada de entender e manter o código. Os programas malignos ignoram os meios básicos da linguagem em favor de técnicas que fornecem benefícios a curto prazo em troca de problemas a longo prazo. Como um breve lembrete, as "práticas" ruins de Go incluem:

  • Pacotes mal nomeados e organizados
  • Interfaces organizadas incorretamente
  • Passando ponteiros para variáveis ​​em funções para preencher seus valores
  • Usando pânico em vez de erros
  • Usando funções init e importações vazias para configurar dependências
  • Baixe arquivos de configuração usando as funções init
  • Usando estruturas em vez de bibliotecas

Grande bola do mal


O que acontece se juntarmos todas as nossas más práticas? Teríamos uma estrutura que usaria muitos arquivos de configuração, preencha os campos da estrutura usando ponteiros, defina interfaces para descrever tipos publicados, confie no código “mágico” e entre em pânico sempre que ocorrer um problema.

E eu fiz isso. Se você acessar https://github.com/evil-go , verá o Fall , uma estrutura de DI que permite implementar as práticas “más” que você deseja. Eu soldei o Fall com uma pequena estrutura da Web Outboy que segue os mesmos princípios.

Você pode perguntar como eles são vilões? Vamos ver Sugiro optar por um programa Go simples (escrito usando as práticas recomendadas) que forneça o ponto de extremidade http. E reescreva-o usando Fall e Outboy.

Melhores práticas


Nosso programa está em um único pacote chamado greet, que usa todas as funções básicas para implementar nosso endpoint. Como este é um exemplo, usamos um DAO de trabalho na memória, com três campos para os valores que retornaremos. Também teremos um método que, dependendo da entrada, substitui a chamada em nosso banco de dados e retorna a saudação desejada.

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 } } 

Em seguida é a lógica de negócios. Para implementá-lo, definimos uma estrutura para armazenar dados de saída, uma interface GreetingFinder para descrever o que a lógica de negócios está procurando no nível de pesquisa de dados e uma estrutura para armazenar a própria lógica de negócios com um campo para o GreetingFinder. A lógica real é simples - apenas chama GreetingFinder e lida com todos os erros que possam ocorrer.

 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 } 

Em seguida, vem a camada da web e, para esta parte, definimos a interface Greeter, que fornece toda a lógica comercial necessária, bem como a estrutura que contém o manipulador http configurado com Greeter. Em seguida, criamos um método para implementar a interface http.Handler, que divide a solicitação http, chama greeter-a (saudação), processa erros e retorna os resultados.

 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)) } 

Este é o fim do pacote greet. A seguir, veremos como um desenvolvedor "bom" do Go escreveria main para usar este pacote. No pacote principal, definimos uma estrutura chamada Config, que contém as propriedades que precisamos executar. A função principal faz três coisas.

  • Primeiro, ele chama a função loadProperties, que usa uma biblioteca simples ( https://github.com/evil-go/good-sample/blob/master/config/config.go ) para carregar as propriedades do arquivo de configuração e as coloca na nossa cópia de uma configuração. Se o download da configuração falhar, a função principal reporta um erro e sai.
  • Em segundo lugar, a função principal liga os componentes no pacote greet, atribuindo-lhes explicitamente valores da configuração e definindo as dependências.
  • Em terceiro lugar, ele chama uma pequena biblioteca de servidores ( https://github.com/evil-go/good-sample/blob/master/server/server.go ) e passa o endereço, o método HTTP e o http.Handler para o endpoint para solicitação de processamento. Uma chamada de biblioteca inicia um serviço da web. E esta é toda a nossa aplicação.

 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) } } 

O exemplo é bem curto, mas mostra como o Go é escrito; algumas coisas são ambíguas, mas em geral fica claro o que está acontecendo. Colamos pequenas bibliotecas que são configuradas especificamente para trabalharem juntas. Nada está oculto; qualquer um pode pegar esse código, entender como suas partes estão conectadas e, se necessário, refazê-las para novas.

Ponto preto


Agora vamos considerar a versão do Fall and Outboy. A primeira coisa que faremos é dividir o pacote greet em vários pacotes, cada um contendo uma camada de aplicativo. Aqui está o pacote DAO. Importa Fall, nossa estrutura de DI, e como somos “maus” e definimos relacionamentos com interfaces de maneira inversa, definiremos uma interface chamada GreetDao. Observe - removemos todos os links para erros; se algo está errado, apenas entramos em pânico. Neste ponto, já temos embalagens ruins, interfaces ruins e bugs ruins. Ótimo começo!

Renomeamos ligeiramente nossa estrutura a partir de um bom exemplo. Os campos agora possuem tags struct; eles são usados ​​para fazer com que Fall defina o valor registrado no campo. Também temos uma função init para o nosso pacote, com a qual acumulamos "poder maligno". Na função init do pacote, chamamos Fall duas vezes:

  • Uma vez para registrar um arquivo de configuração que fornece valores para tags de estrutura.
  • E outro, para registrar um ponteiro para uma instância da estrutura. O Fall poderá preencher esses campos para nós e disponibilizar o DAO para uso por outro código.

 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{}) } 

Vamos ver o pacote de serviços. Ele importa o pacote DAO porque precisa acessar a interface definida lá. O pacote de serviço também importa o pacote de modelo, que ainda não consideramos - armazenaremos nossos tipos de dados lá. E importamos o Fall, porque, como todos os frameworks "bons", ele penetra em todos os lugares. Também definimos uma interface para o serviço para dar acesso à camada da web. Novamente, sem manipulação de erros.

A implementação do nosso serviço agora tem uma etiqueta estrutural com fio. O fio marcado no campo conecta automaticamente sua dependência quando a estrutura é registrada no outono. Em nosso pequeno exemplo, é claro o que será atribuído a esse campo. Mas em um programa maior, você saberá apenas que em algum lugar essa interface do GreetDao está implementada e está registrada no outono. Você não pode controlar o comportamento da dependência.

A seguir, está o método de nosso serviço, que foi ligeiramente modificado para obter a estrutura GreetResponse do pacote de modelos e que remove qualquer tratamento de erro. Finalmente, temos uma função init no pacote que registra uma instância de serviço no outono.

 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{}) } 

Agora vamos ver o pacote do modelo. Não há nada especialmente para se olhar. Pode-se ver que o modelo é separado do código que o cria, apenas para dividir o código em camadas.

 package model type GreetResponse struct { Message string } 

No pacote da web, temos uma interface da web. Aqui importamos Fall e Outboy e também importamos o pacote de serviços do qual o pacote da web depende. Como as estruturas só funcionam bem juntas quando são integradas nos bastidores, o Fall possui um código especial para garantir que ela e o Outboy funcionem juntos. Também estamos alterando a estrutura para que ela se torne o controlador de nosso aplicativo da web. Ela tem dois campos:

  • O primeiro é conectado através do Fall à implementação da interface GreetService a partir do pacote de serviços.
  • O segundo é o caminho para o nosso único terminal da Web. É atribuído o valor do arquivo de configuração registrado na função init deste pacote.

Nosso manipulador http foi renomeado GetHello e agora está livre de manipulação de erros. Também temos o método Init (com letra maiúscula), que não deve ser confundido com a função init. Init é um método mágico chamado de estruturas registradas no outono após o preenchimento de todos os campos. Em Init, chamamos Outboy para registrar nosso controlador e seu ponto final no caminho que foi definido usando Fall. Observando o código, você verá o caminho e o manipulador, mas o método HTTP não está especificado. No Outboy, o nome do método é usado para determinar a qual método HTTP o manipulador responde. Como nosso método é chamado GetHello, ele responde às solicitações GET. Se você não conhece essas regras, não poderá entender quais solicitações ele responde. É verdade que isso é muito vilão?

Finalmente, chamamos a função init para registrar o arquivo de configuração e o controlador no outono.

 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{}) } 

Resta apenas mostrar como executamos o programa. No pacote principal, usamos importações vazias para registrar o Outboy e o pacote da web. E a função principal chama fall.Start () para iniciar o aplicativo inteiro.

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

Rompimento do tegumento


E aqui está, um programa completo escrito usando todas as nossas ferramentas Go mal. Isso é um pesadelo. Ela oculta magicamente como as partes do programa se encaixam e torna terrivelmente difícil entender seu trabalho.

E, no entanto, você deve admitir que há algo atraente em escrever código com Fall e Outboy. Para um pequeno programa, você pode até dizer que é uma melhoria. Veja como é fácil configurar! Eu posso conectar dependências com quase nenhum código! Registrei um manipulador para o método, apenas usando seu nome! E sem nenhum tratamento de erros, tudo parece tão limpo!

É assim que o mal funciona. À primeira vista, é realmente atraente. Mas, à medida que seu programa muda e cresce, toda essa mágica começa a interferir, complicando a compreensão do que está acontecendo. Somente quando você está completamente obcecado pelo mal, você olha para trás e percebe que está preso.

Para desenvolvedores Java, isso pode parecer familiar. Essas técnicas podem ser encontradas em muitas estruturas Java populares. Como mencionei anteriormente, trabalho com Java há mais de 20 anos, a partir da 1.0.2 em 1996. Em muitos casos, os desenvolvedores Java foram os primeiros a encontrar problemas ao criar software corporativo em larga escala na era da Internet. Lembro-me dos momentos em que servlets, EJB, Spring e Hibernate apareceram. As decisões que os desenvolvedores Java tomaram naquele momento faziam sentido. Mas, ao longo dos anos, essas técnicas mostram sua idade. Idiomas mais recentes, como Go, foram projetados para eliminar os pontos problemáticos encontrados ao usar técnicas mais antigas. No entanto, à medida que os desenvolvedores de Java começam a aprender Go e escrever código com ele, eles devem se lembrar de que tentar reproduzir padrões do Java produzirá maus resultados.

O Go foi desenvolvido para programação séria - para projetos que abrangem centenas de desenvolvedores e dezenas de equipes. Mas para o Go fazer isso, você precisa usá-lo da maneira que funciona melhor. Podemos escolher ser bons ou maus. Se escolhermos o mal, podemos incentivar os jovens desenvolvedores do Go a mudarem seu estilo e técnicas antes de entenderem o Go. Ou podemos escolher o bem. Parte do nosso trabalho como desenvolvedores do Go é educar jovens Gopher (Gopher), para ajudá-los a entender os princípios subjacentes às nossas melhores práticas.

A única desvantagem de seguir o caminho do bem é que você precisa procurar outra maneira de expressar seu mal interior. Talvez tente dirigir a uma velocidade de 30 km / h na rodovia federal?

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


All Articles