Implementación del patrón Functional Options en Go

Cuando diseñas una librería o un componente de infraestructura, te enfrentas inevitablemente a la evolución de la configuración. Empezar con un constructor simple como NewServer(host, port) es fácil, pero a medida que el sistema crece, terminas con firmas de funciones inmanejables: NewServer(host, port, timeout, maxConns, tlsConfig, logger, retryPolicy, ...). Este fenómeno, conocido como constructor bloat, hace que el código sea frágil: el orden de los parámetros importa y cualquier cambio en la firma rompe todos los llamadores existentes.

El Functional Options pattern es una solución elegante para construir objetos con parámetros opcionales de forma extensible. En lugar de pasar una lista interminable de valores, pasas una serie de funciones que modifican el objeto durante su inicialización. Internamente, definimos un tipo función Option que actúa sobre el struct que queremos configurar. El constructor acepta una lista variádica de estas funciones (...Option) y las aplica sobre una instancia con valores por defecto.

Este patrón se utiliza cuando necesitas un constructor que sea compatible con el futuro: si mañana añades un nuevo campo a tu struct, solo tienes que añadir una nueva función WithX(...) sin romper el código de nadie. Sin embargo, si intentas usar este patrón para parámetros que son estrictamente obligatorios, pierdes la capacidad del compilador para asegurar que el usuario los proporcione, moviendo un error de diseño a un error de tiempo de ejecución.

package main

import (
	"fmt"
	"time"
)

// Server representa un servidor de red con múltiples configuraciones.
type Server struct {
	addr         string
	timeout      time.Duration
	maxConns     int
	idleTimeout  time.Duration
}

// Option define la firma de la función que aplicará cambios al Server.
type Option func(*Server)

// WithTimeout permite configurar el tiempo de espera de las peticiones.
func WithTimeout(d time.Duration) Option {
	return func(s *Server) {
		s.timeout = d
	}
}

// WithMaxConns configura el límite de conexiones concurrentes.
func WithMaxConns(n int) Option {
	return func(s *Server) {
		s.maxConns = n
	}
}

// NewServer es el constructor que implementa el patrón.
// El parámetro 'addr' es obligatorio, mientras que 'opts' es opcional.
func NewServer(addr string, opts ...Option) *Server {
	// Definimos valores por defecto razonables.
	s := &Server{
		addr:        addr,
		timeout:     30 * time.Second,
		maxConns:    10,
		idleTimeout: 5 * time.Minute,
	}

	// Aplicamos cada opción proporcionada por el usuario.
	// El orden de aplicación de las opciones importa si dos opciones
	// modifican el mismo campo.
	for _, opt := range opts {
		opt(s)
	}

	return s
}

func main() {
	// Caso 1: Uso mínimo, solo con parámetros obligatorios.
	srvMin := NewServer("127.0.0.1:8080")
	fmt.Printf("Mínimo: %+v\n", srvMin)

	// Caso 2: Configuración personalizada.
	srvCustom := NewServer(
		"0.0.0.0:9000",
		WithTimeout(10*time.Second),
		WithMaxConns(100),
	)
	fmt.Printf("Personalizado: %+v\n", srvCustom)
}

Análisis del código

En el ejemplo, el corazón del patrón reside en la definición de Option func(*Server). Esta es una función de orden superior: WithTimeout no modifica el servidor directamente, sino que retorna una función que sabe cómo modificar un *Server. Esta técnica de “closure” permite que la función WithTimeout capture el valor d y lo inyecte en el struct cuando el constructor la ejecute.

Dentro de NewServer, primero inicializamos la estructura s con valores por defecto. Esto garantiza que, aunque el usuario no pase ninguna opción, el objeto sea válido y consistente. Luego, recorremos la rebanada opts con un for range. Si el usuario llama a NewServer("addr", WithTimeout(5s), WithTimeout(10s)), la última opción aplicada prevalecerá, ya que simplemente sobreescribe el campo en el struct.

El uso de ...Option (variadic parameters) es clave para la flexibilidad: permite llamar al constructor con cero, uno o múltiples opciones de forma limpia.

El error frecuente

Un error de diseño común es intentar convertir todos los parámetros en Option, incluso los que son esenciales para el funcionamiento del objeto.

// ERROR: Diseño incorrecto para parámetros obligatorios
func NewServerBad(opts ...Option) *Server {
    s := &Server{}
    for _, opt := range opts {
        opt(s)
    }
    // ¿Cómo garantizamos que 'addr' fue seteado? 
    // El compilador no nos ayuda aquí.
    return s
}

Si haces esto, un programador podría instanciar un Server sin dirección IP y tu código fallará más adelante en el tiempo de ejecución con un error difícil de rastrear. La regla de oro es: los parámetros obligatorios van en el cuerpo del constructor y los opcionales se pasan mediante Option.

225

Dejar un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Scroll al inicio