Option Pattern en Golang

Ha pasado alrededor de un año desde que empecé a desarrollar algunas cosas en Go. Al principio, era un lenguaje desconocido para mí. Solo sabía que era un lenguaje “tipado”, compilado y de memoria segura a través de un GC (Garbage collector), nada más. Así que hice un curso acelerado para aprender sus tipos, su sintaxis, la gestión de concurrencia y de errores, etc.

Una de las cosas que he aprendido es el patrón Option Pattern, el cual se utiliza para simplificar la construcción de un struct con parámetros opcionales, ya que Go carece de sobrecarga, es decir, no podemos tener una función con el mismo nombre con distintos argumentos como, por ejemplo, sí encontramos en Java.

Planteamiento del problema

Tenemos un struct llamado server, el cual es privado de su paquete y tiene 4 propiedades que también son privadas (los tipos o propiedades que empiezan en minúscula, en Go, son privadas).

// server struct. We want explicitly to keep properties private
type server struct {
  host       string
  port       uint16
  timeout_ms uint64
  encryption bool
}

Go, en realidad no soporta constructores, pero llamamos constructores a las funciones Factory para instanciar los structs. Solo vamos a poder instanciar este struct de esta forma ya que server es privado. La solución obvia sería crear un constructor con todos los parámetros:

func NewServer(host string, port uint16, ms uint64, encryption bool) *server {
  return &server{
     host:       host,
     port:       default_port,
     timeout_ms: default_timeout,
     encryption: default_encryption,
  }
}

Esto es completamente válido, pero… estamos requiriendo al usuario proporcionar todos y cada uno de los argumentos al constructor. Además, esto puede hacer perder legibilidad al código, ya que varios parámetros son numéricos, y es necesario recordar su orden.

s := NewServer("localhost", 10000, 10000, true)

Planteamiento de la solución

 ¿Verdad que es posible que no recordemos en qué posición estaban los parámetros numéricos? ¿Qué argumento es true?… ¿Qué podemos hacer para mejorar esto? Veamos unos ejemplos.

s := NewServer("localhost", WithEncryption())
s := NewServer("localhost", WithPort(10000), WithTimeoutMs(1000))

En este nuevo constructor, solo estamos obligados a proporcionar el host y opcionalmente, una lista de opciones. Veamos cómo podemos implementar este patrón para crear una API elegante y fácil de usar.

Implementación de la solución

A continuación, implementaremos todo lo necesario para poder utilizar la sintaxis propuesta en el planteamiento de la solución.

Creación de los valores por defecto

const (
  default_port       = 8080
  default_timeout    = 60000
  default_encryption = false
)

Creación del tipo serverOpt

type serverOpt func(*server)

Implementación del constructor

func NewServer(host string, opts ...serverOpt) *server {
  server := &server{
     host,
     default_port,
     default_timeout,
     default_encryption,
  }
  // Iterate options and apply them
  for _, opt := range opts {
     opt(server)
  }
  return server
}

Implementación de las opciones

// WithPort determines port for listen to
func WithPort(port uint16) serverOpt {
  return func(s *server) {
     s.port = port
  }
}

// WithTimeoutMs determines millis to timeout
func WithTimeoutMs(ms uint64) serverOpt {
  return func(s *server) {
     s.timeout_ms = ms
  }
}

// WithEncryption is to enable server encryption
func WithEncryption() serverOpt {
  return func(s *server) {
     s.encryption = true
  }
}

// WithoutEncryption is to disable server encryption
func WithoutEncryption() serverOpt {
  return func(s *server) {
     s.encryption = false
  }
}

Puedes ver el código completo aquí.

¡Esto es todo! Si este post te ha parecido interesante, te animamos a visitar la categoría Software para ver todos los posts relacionados y a compartirlo en redes. ¡Hasta pronto!
Cristòfol Torrens
Cristòfol Torrens
Artículos: 8