Extraños familiares o una vez más sobre el uso de patrones de diseño

Sobre el tema de los patrones de diseño, se han escrito toneladas de artículos y se han publicado muchos libros. Sin embargo, este tema no deja de ser relevante, ya que los patrones nos permiten usar soluciones listas y probadas, lo que nos permite reducir el tiempo de desarrollo del proyecto al mejorar la calidad del código y reducir las deudas técnicas.


Desde la llegada de los patrones de diseño, siempre hay nuevos ejemplos de su uso efectivo. Y esto es maravilloso. Sin embargo, hubo una mosca en la pomada: cada idioma tiene sus propios detalles. Y golang, y aún más (ni siquiera tiene un modelo OOP clásico). Por lo tanto, hay variaciones de los patrones, en relación con los lenguajes de programación individuales. En este artículo, me gustaría tocar el tema de los patrones de diseño en relación con el golang.


Decorador


La plantilla Decorador le permite conectar un comportamiento adicional al objeto (estática o dinámicamente) sin afectar el comportamiento de otros objetos de la misma clase. Una plantilla se usa a menudo para cumplir con el Principio de responsabilidad única, ya que le permite compartir la funcionalidad entre clases para resolver problemas específicos.

El conocido patrón DECORATOR se usa ampliamente en muchos lenguajes de programación. Entonces, en Golang, todo el middleware se construye sobre su base. Por ejemplo, el perfil de consultas podría verse así:


func ProfileMiddleware(next http.Handler) http.Handler { started := time.Now() next.ServeHTTP() elapsed := time.Now().Sub(started) fmt.Printf("HTTP: elapsed time %d", elapsed) } 

En este caso, la interfaz del decorador es la única función. Como regla, esto debe buscarse. Sin embargo, un decorador con una interfaz más amplia a veces puede ser útil. Por ejemplo, considere el acceso a una base de datos (paquete de base de datos / sql). Supongamos que necesitamos hacer el mismo perfil de las consultas de la base de datos. En este caso, necesitamos:


  • En lugar de interactuar directamente con la base de datos a través de un puntero, debemos ir a la interacción a través de la interfaz (para separar el comportamiento de la implementación).
  • Cree un contenedor para cada método que ejecute una consulta de base de datos SQL.

Como resultado, obtenemos un decorador que le permite perfilar todas las consultas a la base de datos. Las ventajas de este enfoque son innegables:


  • Mantiene la limpieza del código del componente de acceso a la base de datos central.
  • Cada decorador implementa un único requisito. Debido a esto, se logra su facilidad de implementación.
  • Debido a la composición de los decoradores, obtenemos un modelo extensible que se adapta fácilmente a nuestras necesidades.
  • Obtenemos una sobrecarga de rendimiento cero en modo de producción debido a un simple apagado del generador de perfiles.

Entonces, por ejemplo, puede implementar los siguientes tipos de decoradores:


  • Latido del corazón Hacer ping a una base de datos para mantener viva una conexión a ella.
  • Profiler. La salida del cuerpo de la solicitud y su tiempo de ejecución.
  • Sniffer Colección de métricas de bases de datos.
  • Clon Clonación de la base de datos original con fines de depuración.

Como regla general, al implementar decoradores ricos, no se requiere la implementación de todos los métodos: es suficiente delegar métodos no implementados a un objeto interno.


Supongamos que necesitamos implementar un registrador avanzado para rastrear consultas DML para una base de datos (para rastrear consultas INSERT / UPDATE / DELETE). En este caso, no necesitamos implementar toda la interfaz de la base de datos, solo superpongamos solo el método Exec.


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

Por lo tanto, vemos que crear incluso un rico decorador en el idioma golang no es particularmente difícil.


Método de plantilla


Método de plantilla (Ing. Método de plantilla): un patrón de diseño de comportamiento que define la base del algoritmo y permite a los herederos redefinir algunos pasos del algoritmo sin cambiar su estructura en su conjunto.

El lenguaje golang admite el paradigma OOP, por lo que esta plantilla no se puede implementar en su forma pura. Sin embargo, nada nos impide improvisar constructores utilizando funciones adecuadas.


Supongamos que necesitamos definir un método de plantilla con la siguiente firma:


 func Method(s string) error 

Al declarar, es suficiente que usemos un campo de tipo funcional. Para la conveniencia de trabajar con él, podemos usar la función de contenedor para complementar la llamada con el parámetro faltante y para crear una instancia específica, la función de constructor correspondiente.


 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) } // First constructor func NewStruct1() *MyStruct { return &MyStruct{ MethodImpl: func(me *MyStruct, s string) error { // Implementation 1 ... }, } } // Second constructor func NewStruct2() *MyStruct { return &MyStruct{ MethodImpl: func(me *MyStruct, s string) error { // Implementation 2 ... }, } } func main() { // Create object instance o := NewStruct2() // Call the template method err := o.Method("hello") ... } 

Como puede ver en el ejemplo, la semántica del uso del patrón casi no es diferente de la OOP clásica.


Adaptador


El patrón de diseño "Adaptador" le permite utilizar la interfaz de una clase existente como otra interfaz. Esta plantilla se usa a menudo para garantizar que algunas clases funcionen con otras sin cambiar su código fuente.

En general, los adaptadores pueden servir, como funciones separadas e interfaces completas. Si con las interfaces todo es más o menos claro y predecible, desde el punto de vista de las funciones individuales hay sutilezas.


Supongamos que escribimos algún servicio que tiene alguna API interna:


 type MyService interface { Create(ctx context.Context, order int) (id int, err error) } 

Si necesitamos proporcionar una API pública con una interfaz diferente (por ejemplo, trabajar con gRPC), entonces simplemente podemos usar las funciones del adaptador que se ocupan de la conversión de la interfaz. Es muy conveniente usar cierres para este propósito.


 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) { // Decode request req := request.(CreateRequest) // Call service method id, err := s.Create(ctx, req.Order) // Encode response return CreateResponse{ID: id, Err: err}, nil } } 

La función makeCreateEndpoint tiene tres pasos estándar:


  • valores de decodificación
  • llamando a un método desde la API interna del servicio que se está implementando
  • codificación de valor

Todos los puntos finales en el paquete gokit se basan en este principio.


Visitante


La plantilla "Visitante" es una forma de separar el algoritmo de la estructura del objeto en el que opera. El resultado de la separación es la capacidad de agregar nuevas operaciones a las estructuras de objetos existentes sin modificarlas. Esta es una forma de cumplir con el principio abierto / cerrado.

Considere el conocido patrón de visitante en el ejemplo de formas geométricas.


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

Supongamos que queremos escribir una estrategia para calcular la distancia desde un punto dado a una forma específica.


 type DistanceStrategy struct { X, Y float32 } func (s *DistanceStrategy) VisitPoint(p *Point) (interface{}, error) { // Evaluate distance from point(X, Y) to point p } func (s *DistanceStrategy) VisitLine(l *Line) (interface{}, error) { // Evaluate distance from point(X, Y) to line l } func (s *DistanceStrategy) VisitCircle(c *Circle) (interface{}, error) { // Evaluate distance from point(X, Y) to circle c } func main() { s := &DistanceStrategy{X: 1, Y: 2} p := &Point{X: 3, Y: 4} res, err := p.Visit(s) if err != nil { panic(err) } fmt.Printf("Distance is %g", res.(float32)) } 

Del mismo modo, podemos implementar otras estrategias que necesitamos:


  • Extensión vertical
  • La extensión horizontal del objeto.
  • Construyendo un cuadrado mínimo (MBR)
  • Otras primitivas que necesitamos.

Además, las figuras previamente definidas (Punto, Línea, Círculo ...) no saben nada sobre estas estrategias. Su único conocimiento se limita a la interfaz GeometryVisitor. Esto le permite aislarlos en un paquete separado.


En un momento, mientras trabajaba en un proyecto cartográfico, tuve la tarea de escribir una función para determinar la distancia entre dos objetos geográficos arbitrarios. Las soluciones eran muy diferentes, pero no todas eran lo suficientemente eficientes y elegantes. Considerando de alguna manera el patrón Visitante, noté que sirve para seleccionar el método de destino y se parece un poco a un paso de recursión separado, que, como saben, simplifica la tarea. Esto me llevó a usar Double Visitor. Imagine mi sorpresa cuando descubrí que ese enfoque no se menciona en absoluto en Internet.


 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) { // Evaluate distance between point and p } func (point *pointStrategy) Visit(l *Line) (interface{}, error) { // Evaluate distance between point and l } func (point *pointStrategy) Visit(c *Circle) (interface{}, error) { // Evaluate distance between point and c } type lineStrategy struct { *Line } func (line *lineStrategy) Visit(p *Point) (interface{}, error) { // Evaluate distance between line and p } func (line *lineStrategy) Visit(l *Line) (interface{}, error) { // Evaluate distance between line and l } func (line *lineStrategy) Visit(c *Circle) (interface{}, error) { // Evaluate distance between line and c } type circleStrategy struct { *Circle } func (circle *circleStrategy) Visit(p *Point) (interface{}, error) { // Evaluate distance between circle and p } func (circle *circleStrategy) Visit(l *Line) (interface{}, error) { // Evaluate distance between circle and l } func (circle *circleStrategy) Visit(c *Circle) (interface{}, error) { // Evaluate distance between circle and c } func Distance(a, b Geometry) (float32, error) { return a.Visit(&geometryStrategy{G: b}) } 

Por lo tanto, hemos construido un mecanismo selectivo de dos niveles, que, como resultado de su trabajo, llamará al método apropiado para calcular la distancia entre dos primitivas. Solo podemos escribir estos métodos y el objetivo se logra. Así es como un problema elegantemente no determinista puede reducirse a una serie de funciones elementales.


Conclusión


A pesar del hecho de que no existe una OOP clásica en el golang, el lenguaje produce su propio dialecto de patrones que juegan con las fortalezas del lenguaje. Estos patrones van desde la negación a la aceptación universal y se convierten en las mejores prácticas con el tiempo.


Si habrozhiteli respetado tiene alguna idea sobre los patrones, no sea tímido y exprese su opinión al respecto.

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


All Articles