La semana pasada Joan nos explicaba como funcionaba el patrón de diseño creacional singleton en Go. Realmente no vamos a tocar todos los patrones de diseño que hay, pero no está mal que conozcamos los más comunes y entendamos y veamos sus implementaciones.
Es por ese motivo que hoy vengo a hablaros de un patrón de diseño, en este caso estructural, llamado Decorator, seguramente os suena mucho si venís de otros lenguajes de programación, así que vamos a ver para que funciona y como podemos implementarlo en Go.
¿Qué es el patrón Decorator?
El patrón Decorator o Decorador te permite decorar, valga la redundancia, cualquier type
que tengas con más funcionalidades sin tener que modificar el type
original. Pensemos en esto en cajas que vamos metiendo unas dentro de otras.
Para lograr esto nuestro Decorator type
implementa la misma interfaz que el type
que decora, y guarda una instancia de ese type
. De esta manera podremos ir apilando tantos decorators
como quieras simplemente pasando el viejo decorator
como campo del nuevo.
Puedes estar pensando que este patrón no tiene mucho sentido si necesito que un type
haga algo específico se lo implemento y solucionado. Pero esto no siempre es posible, pensemos por ejemplo en un código legacy, que no tiene tests y tenemos mucho miedo de romper, podríamos hacer uso de lo anteriormente mencionado para dotarle de nuevas funcionalidades sin preocuparnos de producir un breaking change.
Así pues podríamos llegar a decir que los criterios para utilizar el patrón Decorator
serían los siguiente:
- En aquel momento que necesites añadir funcionalidad a un código sobre el cual no tengas acceso o tengas miedo de poder causar algún breaking change desafortunado.
- O cuando quieras añadir funcionalidad a un
type
que será creado o modificado dinámicamente y el número de funcionalidades sea desconocido y pueda crecer demasiado.
Ahora que ya tenemos claro, para que sirve y cuando usarlo vamos a pasar a lo que más nos llama la atención, ponerlo en funcionamiento.
Implementando el patrón Decorator
Para la implementación podríamos usar el ejemplo clásico de montar una hamburguesa o una pizza, pero yo soy de la creencia de que utilizar ejemplos más reales que nos podemos encontrar en nuestro día a día es mucho mejor para asimilar un concepto.
Muchos sois familiares a la arquitectura hexagonal, y sus application services, que no son más que servicios que nos van a ayudar a comunicar nuestra capa de infrastructura con nuestra capa de dominio.
Pensemos por ejemplo que tenemos un application service
que nos permite crear gophers, podríamos tener un código similar al siguiente.
package creator
type Service interface {
CreateGopher(id string) error
}
type service struct{
repository storage.GopherRepository
}
func NewService(repository storage.GopherRepository) Service {
return service{repository: repository}
}
func (s service) CreateGopher(id string) error {
// do something
return nil
}
Posteriormente alguien necesitaría instanciarlo para poder comenzar a utilizarlo:
...
repository := mongo.NewGopherRepository()
creatorService := creator.NewService(repository)
...
Pero ahora nos ha surgido la necesidad de a nuestro servicio añadirle logging e instrumentación, es decir queremos tener la capacidad de poder logar lo que sucede en nuestro servicio y además queremos poder monitorearlo.
Lo primero que nos vendría a la cabeza sería, añadir nuevas dependencias dentro de nuestro struct
, pero aquí podemos utilizar el patrón decorator
y desacoplar así completamente estas funcionalidades de nuestro application services
.
Pongamos por ejemplo la implementación de logging.
package creator
type loggingService struct {
logger *zap.Logger
next Service
}
func NewLoggingService(logger *zap.Logger, service Service) Service {
return loggingService{logger: logger, service: service}
}
func (s loggingService) CreateGopher(id string) error {
logger.Info("log something")
err := s.next.CreateGopher(id)
if err != nil {
logger.Error("some related with this error msg", err)
return err
}
return err
}
De esta manera hemos añadido la posibilidad de logar aquello que necesitemos sin modificar nuestro servicio original. Sino que le hemos añadido una decoración que se encargará de hacerlo por nosotros y llamar al método original cuando lo vea conveniente.
Para aplicar esta decoración simplemente tendremos que añadirlo al momento la instanciación.
...
repository := mongo.NewGopherRepository()
creatorService := creator.NewService(repository)
creatorLoggingService := creator.NewLoggingService(logger, creatorService)
...
Y a partir de ahora utilizaremos el creatorLoggingService
allá donde necesitamos un creator.Service
ya que cumple con nuestra interface
y sabemos que se encargará de llamar al servicio original.
Conclusión
Ahora es tu turno, ya sabes como dotar de nuevas funcionalidades a tus funcionalidades antiguas o aquellas que no puedas acceder, incluso desacoplar ciertos comportamientos como el caso que hemos mostrado. Las posibilidades son infinitas, os chivamos un spoiler, los middlewares funcionan utilizando el mismo patrón.
Recordad que para cualquier duda podéis dejar vuestros comentarios aquí mismo o en nuestra cuenta de twitter oficial @FriendsOfGoTech, estaremos encantados de ayudaros.