Gustavo Catillo | Software Engineer

Functional options

13 febrero, 2021

Foto por: Katie Burandt

Recientemente me encontraba escribiendo un paquete en Go y me vi en la necesidad de proveer al usuario de mi paquete la posibilidad de pasar algún tipo de configuración personalizada o bien usar las que mi paquete ya provee.

Al principio pensé en usar algunas variables globales (lo cual casi siempre es una mala idea 👎🏽) las cuales el usuario pudiera sobreescribir o bien usar los valores que yo estableciera en esas variables. Sin embargo después de investigar un poco me encotré con el concepto de Functional Options, veamos como usar esta técnica.

El problema

Supongamos que queremos diseñar un paquete que nos permita saludar a un usuario en base a un nombre que ellos nos pasen, además queremos que los usuarios puedan pasarnos saludos personalizados o usar los que nosotros coloquemos por defecto.

Functional Options al rescate

Para esta solución lo que vamos a hacer es crear un tipo llamado Option, este es un enfoque de programación funcional el cual nos permite pasar n número de Options como funciones e ir aplicando esas opciones a la estructura que estemos usando, en este caso Greeter, esto nos da la oportunidad de establecer valores que el usuario nos pase (que es lo que queremos) o usar valores por defecto que nosotros decidamos.

// Package greeter handles greets for users.
package greeter

import "fmt"

// Greeter knows how to greet users.
type Greeter struct {
	Greeting string
	Message  string
}

// Option defines an option type function, to handle multiple options.
type Option func(*Greeter)

// WithGreeting knows how to set a custom Greeting for our Greeter.
func WithGreeting(greet string) Option {
	return func(g *Greeter) {
		g.Greeting = greet
	}
}

// WithMessage knows how to set a custom Message for our Greeter.
func WithMessage(message string) Option {
	return func(g *Greeter) {
		g.Message = message
	}
}

// New creates a new Greeter with some default values.
func New(options ...Option) *Greeter {
	g := &Greeter{
		Greeting: "Hello",
		Message:  "Nice to meet you!.",
	}

	for __, option := range options {
		option(g)
	}

	return g
}

// Greet knows how to greet an user.
func (g Greeter) Greet(name string, args ...string) {
	fmt.Printf("%s %s %s\n", g.Greeting, name, g.Message)
}

Una vez creado nuestro paquete greeter procedemos a usarlo en nuestro paquete main de la siguiente forma, en el primer ejemplo vemos como usar los valores por defecto que establecimos en el paquete greeter únicamente llamando a la función New (sin ninguna opción) y posteriormente llamando al método Greet con el valor de “John Doe”.

En el segundo ejemplo vemos cómo pasar algunas function options al momento de llamar a la función New en este caso estamos estableciendo los valores que necesitemos para el Message y Greeting.

package main

import "github.com/gustavocd/code-club/options/greeter"

func main() {
	// Using default values provided by our Option functions.
	dg := greeter.New()
	dg.Greet("John Doe")

	// Using custom config values via our Option functions.
	g := greeter.New(
		greeter.WithGreeting("Hola"),
		greeter.WithMessage("Un placer conocerte!."),
	)
	g.Greet("Gustavo")
}

Conclusión

Si bien es cierto que con esta solución la legibilidad de nuestro código aumenta en complejidad, dado que no todos estamos familiarizados con la programación funcional y para los que son nuevos en Go puede llegar a ser complicado de entender.

Me parece una solución que nos da un buen balance entre legibilidad para el programador que crea el paquete y una buena experiencia de usuario para el programador que usa nuestro paquete, así que de momento es un precio que estoy dispuesto a pagar.

Recursos útiles para aprender más sobre este tema


© 2021, Sígueme en: |