Malos consejos para un programador de Go
En la primera parte de la publicación, expliqué cómo convertirse en un programador Go "malicioso". El mal viene en muchas formas, pero en la programación radica en la dificultad deliberada de comprender y mantener el código. Los programas malvados ignoran los medios básicos del lenguaje a favor de las técnicas que proporcionan beneficios a corto plazo a cambio de problemas a largo plazo. Como breve recordatorio, las "prácticas" malvadas de Go incluyen:
- Paquetes mal nombrados y organizados
- Interfaces mal organizadas
- Pasar punteros a variables en funciones para llenar sus valores
- Usando pánico en lugar de errores
- Usar funciones de inicio e importaciones vacías para configurar dependencias
- Descargue archivos de configuración utilizando las funciones de inicio
- Usar marcos en lugar de bibliotecas
Gran bola del mal
¿Qué pasa si juntamos todas nuestras malas prácticas? Tendríamos un marco de trabajo que usaría muchos archivos de configuración, completaría los campos de estructura utilizando punteros, definiría interfaces para describir los tipos publicados, se basaría en código "mágico" y entraría en pánico cuando ocurriera un problema.
Y lo hice Si va a
https://github.com/evil-go , verá
Fall , un marco de DI que le permite implementar las prácticas "malvadas" que desee. He soldado Fall con un pequeño framework web Outboy que sigue los mismos principios.
Puedes preguntar qué tan villanos son? A ver Sugiero ir a un programa simple de Go (escrito utilizando las mejores prácticas) que proporciona el punto final http. Y luego reescríbalo usando Fall and Outboy.
Mejores prácticas
Nuestro programa se encuentra en un único paquete llamado greet, que utiliza todas las funciones básicas para implementar nuestro punto final. Como este es un ejemplo, utilizamos un DAO de memoria en funcionamiento, con tres campos para los valores que devolveremos. También tendremos un método que, dependiendo de la entrada, reemplaza la llamada a nuestra base de datos y devuelve el saludo deseado.
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 } }
Lo siguiente es la lógica de negocios. Para implementarlo, definimos una estructura para almacenar datos de salida, una interfaz de GreetingFinder para describir lo que la lógica de negocios está buscando en el nivel de búsqueda de datos y una estructura para almacenar la lógica de negocios con un campo para GreetingFinder. La lógica real es simple: solo llama a GreetingFinder y maneja cualquier error que pueda ocurrir.
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 }
Luego viene la capa web, y para esta parte definimos la interfaz Greeter, que proporciona toda la lógica de negocios que necesitamos, así como la estructura que contiene el controlador http configurado usando Greeter. Luego creamos un método para implementar la interfaz http.Handler, que divide la solicitud http, llama a greeter-a (saludo), procesa errores y devuelve los 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 es el final del paquete de bienvenida. A continuación, veremos cómo un desarrollador de Go "bueno" escribiría main para usar este paquete. En el paquete principal, definimos una estructura llamada Config, que contiene las propiedades que necesitamos ejecutar. La función principal hace 3 cosas.
- Primero, llama a la función loadProperties, que usa una biblioteca simple ( https://github.com/evil-go/good-sample/blob/master/config/config.go ) para cargar propiedades del archivo de configuración y las coloca en nuestra copia de una configuración. Si la descarga de la configuración falla, la función principal informa un error y se cierra.
- En segundo lugar, la función principal vincula los componentes en el paquete greet, asignándoles explícitamente valores de la configuración y configurando las dependencias.
- En tercer lugar, llama a una pequeña biblioteca de servidor ( https://github.com/evil-go/good-sample/blob/master/server/server.go ) y pasa la dirección, el método HTTP y http.Handler al punto final para procesamiento de solicitudes. Una llamada a la biblioteca inicia un servicio web. Y esta es toda nuestra aplicación.
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) } }
El ejemplo es bastante corto, pero muestra lo genial que está escrito Go; Algunas cosas son ambiguas, pero en general está claro lo que está sucediendo. Pegamos bibliotecas pequeñas que están configuradas específicamente para trabajar juntas. Nada está oculto; cualquiera puede tomar este código, comprender cómo están conectadas sus partes y, si es necesario, rehacerlas a otras nuevas.
Punto negro
Ahora consideraremos la versión de Fall and Outboy. Lo primero que haremos es dividir el paquete de saludo en varios paquetes, cada uno de los cuales contiene una capa de aplicación. Aquí está el paquete DAO. Importa Fall, nuestro marco DI, y dado que somos "malvados" y definimos las relaciones con las interfaces al revés, definiremos una interfaz llamada GreetDao. Tenga en cuenta que hemos eliminado todos los enlaces a errores; Si algo está mal, simplemente entramos en pánico. En este punto, ya tenemos un mal empaquetado, malas interfaces y errores. Gran comienzo!
Cambiamos ligeramente el nombre de nuestra estructura a partir de un buen ejemplo. Los campos ahora tienen etiquetas de estructura; se usan para hacer que Fall establezca el valor registrado en el campo. También tenemos una función init para nuestro paquete, con el cual acumulamos "poder maligno". En la función init del paquete, llamamos a Fall dos veces:
- Una vez para registrar un archivo de configuración que proporciona valores para las etiquetas de estructura.
- Y otro, registrar un puntero a una instancia de la estructura. Fall podrá completar estos campos para nosotros y hacer que el DAO esté disponible para que lo use otro 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{}) }
Veamos el paquete de servicios. Importa el paquete DAO porque necesita acceso a la interfaz definida allí. El paquete de servicios también importa el paquete modelo, que aún no hemos considerado: almacenaremos nuestros tipos de datos allí. E importamos Fall porque, como todos los marcos "buenos", penetra en todas partes. También definimos una interfaz de servicio para dar acceso a la capa web. De nuevo, sin errores de manejo.
La implementación de nuestro servicio ahora tiene una etiqueta estructural con cable. El cable marcado en el campo conecta automáticamente su dependencia cuando la estructura se registra en otoño. En nuestro pequeño ejemplo, está claro qué se asignará a este campo. Pero en un programa más grande, solo sabrá que en algún lugar se implementa esta interfaz GreetDao, y se registra en otoño. No puede controlar el comportamiento de dependencia.
El siguiente es el método de nuestro servicio, que se ha modificado ligeramente para obtener la estructura GreetResponse del paquete del modelo, y que elimina cualquier manejo de errores. Finalmente, tenemos una función init en el paquete que registra una instancia de servicio en Fall.
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{}) }
Ahora veamos el paquete del modelo. Especialmente no hay nada que mirar. Se puede ver que el modelo está separado del código que lo crea, solo para dividir el código en capas.
package model type GreetResponse struct { Message string }
En el paquete web tenemos una interfaz web. Aquí importamos Fall y Outboy, y también importamos el paquete de servicio del que depende el paquete web. Debido a que los marcos solo funcionan bien juntos cuando se integran entre bastidores, Fall tiene un código especial para asegurarse de que él y Outboy trabajen juntos. También estamos cambiando la estructura para que se convierta en el controlador de nuestra aplicación web. Ella tiene dos campos:
- El primero se conecta a través de Fall a la implementación de la interfaz GreetService desde el paquete de servicio.
- El segundo es el camino para nuestro único punto final web. Se le asigna el valor del archivo de configuración registrado en la función init de este paquete.
Nuestro controlador http ha sido renombrado GetHello y ahora está libre de manejo de errores. También tenemos el método Init (con mayúscula), que no debe confundirse con la función init. Init es un método mágico que se llama para estructuras registradas en otoño después de completar todos los campos. En Init, llamamos a Outboy para registrar nuestro controlador y su punto final en la ruta que se configuró con Fall. Al mirar el código, verá la ruta y el controlador, pero el método HTTP no está especificado. En Outboy, el nombre del método se utiliza para determinar a qué método HTTP responde el controlador. Como nuestro método se llama GetHello, responde a las solicitudes GET. Si no conoce estas reglas, no podrá comprender qué solicitudes responde. Es cierto, esto es muy malo?
Finalmente, llamamos a la función init para registrar el archivo de configuración y el controlador en otoño.
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{}) }
Solo queda mostrar cómo ejecutamos el programa. En el paquete principal, utilizamos importaciones vacías para registrar Outboy y el paquete web. Y la función principal llama a fall.Start () para iniciar toda la aplicación.
package main import ( _ "github.com/evil-go/evil-sample/web" "github.com/evil-go/fall" _ "github.com/evil-go/outboy" ) func main() { fall.Start() }
Interrupción del tegumento.
Y aquí está, un programa completo escrito usando todas nuestras malvadas herramientas Go. Esto es una pesadilla Ella oculta mágicamente cómo encajan las partes del programa y hace que sea terriblemente difícil entender su trabajo.
Y, sin embargo, debes admitir que hay algo atractivo en escribir código con Fall and Outboy. Para un programa pequeño, incluso podría decir que es una mejora. ¡Mira lo fácil que es configurarlo! ¡Puedo conectar dependencias casi sin código! ¡Registré un controlador para el método, solo usando su nombre! Y sin ningún error en el manejo, ¡todo se ve tan limpio!
Así es como funciona el mal. A primera vista, es realmente atractivo. Pero a medida que su programa cambia y crece, toda esta magia solo comienza a interferir, lo que complica la comprensión de lo que está sucediendo. Solo cuando estás completamente obsesionado con el mal, miras hacia atrás y te das cuenta de que estás atrapado.
Para los desarrolladores de Java, esto puede parecer familiar. Estas técnicas se pueden encontrar en muchos frameworks Java populares. Como mencioné anteriormente, he estado trabajando con Java durante más de 20 años, comenzando desde 1.0.2 en 1996. En muchos casos, los desarrolladores de Java fueron los primeros en encontrar problemas para escribir software empresarial a gran escala en la era de Internet. Recuerdo los momentos en que los servlets, EJB, Spring e Hibernate acababan de aparecer. Las decisiones que tomaron los desarrolladores de Java en ese momento tenían sentido. Pero con los años, estas técnicas muestran su edad. Los lenguajes más nuevos, como Go, están diseñados para eliminar los puntos débiles encontrados al usar técnicas más antiguas. Sin embargo, a medida que los desarrolladores de Java comienzan a aprender Go y escribir código con él, deben recordar que intentar reproducir patrones de Java producirá malos resultados.
Go fue diseñado para una programación seria, para proyectos que abarcan cientos de desarrolladores y docenas de equipos. Pero para que Go haga esto, debe usarlo de la manera que funcione mejor. Podemos elegir ser malvados o buenos. Si elegimos el mal, podemos alentar a los jóvenes desarrolladores de Go a cambiar su estilo y técnicas antes de que comprendan Go. O podemos elegir bien. Parte de nuestro trabajo como desarrolladores de Go es educar a los jóvenes Gophers (Gophers), para ayudarlos a comprender los principios que subyacen a nuestras mejores prácticas.
El único inconveniente de seguir el camino del bien es que debes buscar otra forma de expresar tu mal interior.
¿Quizás intente conducir a una velocidad de 30 km / h en la carretera federal?