Menu
Coddy logo textTech

Patrón Functional Options

Parte de la sección Programación Orientada a Objetos del Journey de GO de Coddy — lección 79 de 107.

Al crear structs con muchos campos de configuración opcionales, las funciones constructoras pueden volverse difíciles de manejar. El Patrón de Opciones Funcionales resuelve esto de manera elegante mediante el uso de funciones para configurar objetos.

Considera una estructura (struct) de servidor con múltiples configuraciones opcionales. En lugar de un constructor con muchos parámetros, definimos un tipo de opción y funciones que devuelven opciones:

type Server struct {
    host    string
    port    int
    timeout int
}

type Option func(*Server)

func WithPort(port int) Option {
    return func(s *Server) {
        s.port = port
    }
}

func WithTimeout(timeout int) Option {
    return func(s *Server) {
        s.timeout = timeout
    }
}

El constructor acepta una lista variádica de opciones y aplica cada una de ellas:

func NewServer(host string, opts ...Option) *Server {
    s := &Server{
        host:    host,
        port:    8080,    // por defecto
        timeout: 30,      // por defecto
    }
    for _, opt := range opts {
        opt(s)
    }
    return s
}

Ahora la creación de servidores se vuelve legible y flexible:

// Usar valores predeterminados
s1 := NewServer("localhost")

// Personalizar opciones específicas
s2 := NewServer("localhost", WithPort(3000), WithTimeout(60))

Este patrón destaca porque quienes lo llaman solo especifican lo que necesitan, los valores predeterminados están claros en un solo lugar y añadir nuevas opciones no rompe el código existente. Es ampliamente utilizado en librerías de Go como grpc y zap.

challenge icon

Desafío

Fácil

¡Vamos a construir una conexión a base de datos configurable utilizando el Functional Options Pattern! Este patrón destaca cuando tienes muchos ajustes opcionales con valores predeterminados sensatos, exactamente lo que necesita una conexión a base de datos.

Organizarás tu código en dos archivos:

  • database.go: Define tu tipo de conexión a base de datos y las funciones de opción.

    Crea un struct DBConnection con estos campos no exportados:

    • host (string)
    • port (int)
    • username (string)
    • password (string)
    • maxConnections (int)
    • timeout (int) - en segundos

    Define un tipo Option como una función que modifica un *DBConnection.

    Crea estas funciones de opción que devuelven un Option:

    • WithPort(port int)
    • WithCredentials(username, password string)
    • WithMaxConnections(max int)
    • WithTimeout(seconds int)

    Crea un constructor NewDBConnection(host string, opts ...Option) que devuelva un *DBConnection. Establece estos valores predeterminados: port 5432, username "admin", password "secret", maxConnections 10, timeout 30. Aplica todas las opciones proporcionadas después de establecer los valores predeterminados.

    Añade un método ConnectionString() que devuelva la información de conexión en este formato:

    [username]:[password]@[host]:[port] (max:[maxConnections], timeout:[timeout]s)
  • main.go: Construye conexiones a bases de datos con varias configuraciones.

    Lee el host, luego lee un recuento de opciones a aplicar. Para cada opción, lee el tipo de opción y su(s) valor(es):

    • port seguido del número de puerto
    • credentials seguido del nombre de usuario y la contraseña (dos líneas)
    • maxconn seguido del número máximo de conexiones
    • timeout seguido del tiempo de espera en segundos

    Crea un DBConnection con las opciones especificadas e imprime su cadena de conexión.

Se proporcionarán las siguientes entradas:

  • Línea 1: Nombre del host
  • Línea 2: Número de opciones
  • Líneas siguientes: Tipo de opción y valor(es)

Por ejemplo, dado:

localhost
0

Tu salida debería ser:

admin:secret@localhost:5432 (max:10, timeout:30s)

Y dado:

db.example.com
2
port
3306
credentials
root
mypassword

Tu salida debería ser:

root:mypassword@db.example.com:3306 (max:10, timeout:30s)

Y dado:

production.server
4
port
5433
credentials
dbuser
securepass123
maxconn
50
timeout
60

Tu salida debería ser:

dbuser:securepass123@production.server:5433 (max:50, timeout:60s)

Observa cómo quienes llaman solo especifican las opciones que necesitan; los ajustes no especificados utilizan valores predeterminados sensatos. ¡Esta es la elegancia del Functional Options Pattern!

Hoja de referencia

El Patrón de Opciones Funcionales proporciona una forma elegante de configurar estructuras con muchos campos opcionales mediante el uso de funciones en lugar de extensos parámetros de constructor.

Define un tipo Option como una función que modifica un puntero a tu estructura:

type Server struct {
    host    string
    port    int
    timeout int
}

type Option func(*Server)

Crea funciones de opción que devuelvan cierres (closures) de tipo Option:

func WithPort(port int) Option {
    return func(s *Server) {
        s.port = port
    }
}

func WithTimeout(timeout int) Option {
    return func(s *Server) {
        s.timeout = timeout
    }
}

El constructor acepta una lista variádica de opciones, establece los valores predeterminados y luego aplica cada opción:

func NewServer(host string, opts ...Option) *Server {
    s := &Server{
        host:    host,
        port:    8080,    // predeterminado
        timeout: 30,      // predeterminado
    }
    for _, opt := range opts {
        opt(s)
    }
    return s
}

El uso es limpio y flexible: quienes llaman a la función solo especifican lo que necesitan:

// Usar valores predeterminados
s1 := NewServer("localhost")

// Personalizar opciones específicas
s2 := NewServer("localhost", WithPort(3000), WithTimeout(60))

Beneficios: los valores predeterminados están centralizados, los llamadores especifican solo lo que necesitan y agregar nuevas opciones no rompe el código existente.

Pruébalo tú mismo

package main

import (
	"bufio"
	"fmt"
	"os"
	"strconv"
	"strings"
)

func main() {
	reader := bufio.NewReader(os.Stdin)

	// Leer host
	host, _ := reader.ReadString('\n')
	host = strings.TrimSpace(host)

	// Leer el número de opciones
	numOptionsStr, _ := reader.ReadString('\n')
	numOptions, _ := strconv.Atoi(strings.TrimSpace(numOptionsStr))

	// Recopilar opciones
	var opts []Option

	for i := 0; i < numOptions; i++ {
		optionType, _ := reader.ReadString('\n')
		optionType = strings.TrimSpace(optionType)

		switch optionType {
		case "port":
			portStr, _ := reader.ReadString('\n')
			port, _ := strconv.Atoi(strings.TrimSpace(portStr))
			// TODO: Añadir la opción WithPort a opts
			_ = port // Elimina esta línea al implementar

		case "credentials":
			username, _ := reader.ReadString('\n')
			username = strings.TrimSpace(username)
			password, _ := reader.ReadString('\n')
			password = strings.TrimSpace(password)
			// TODO: Añadir la opción WithCredentials a opts
			_, _ = username, password // Elimina esta línea al implementar

		case "maxconn":
			maxStr, _ := reader.ReadString('\n')
			maxConn, _ := strconv.Atoi(strings.TrimSpace(maxStr))
			// TODO: Añadir la opción WithMaxConnections a opts
			_ = maxConn // Elimina esta línea al implementar

		case "timeout":
			timeoutStr, _ := reader.ReadString('\n')
			timeout, _ := strconv.Atoi(strings.TrimSpace(timeoutStr))
			// TODO: Añadir la opción WithTimeout a opts
			_ = timeout // Elimina esta línea al implementar
		}
	}

	// TODO: Crear DBConnection usando NewDBConnection con host y opts
	// TODO: Imprimir la cadena de conexión usando el método ConnectionString()

	_ = opts // Elimina esta línea al implementar
	fmt.Println("TODO: Print connection string here")
}
quiz iconPonte a prueba

Esta lección incluye un breve cuestionario. Empieza la lección para responderlo y registrar tu progreso.

Todas las lecciones de Programación Orientada a Objetos