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.
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
DBConnectioncon estos campos no exportados:host(string)port(int)username(string)password(string)maxConnections(int)timeout(int) - en segundos
Define un tipo
Optioncomo 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: port5432, username"admin", password"secret", maxConnections10, timeout30. 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):
portseguido del número de puertocredentialsseguido del nombre de usuario y la contraseña (dos líneas)maxconnseguido del número máximo de conexionestimeoutseguido del tiempo de espera en segundos
Crea un
DBConnectioncon 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
0Tu salida debería ser:
admin:secret@localhost:5432 (max:10, timeout:30s)Y dado:
db.example.com
2
port
3306
credentials
root
mypasswordTu 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
60Tu 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")
}
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
1Fundamentos de POO en Go
Archivos externosEspacio de trabajo y módulos de GoPaquetes e importacionesNombres exportados vs. no exportadosIntroducción a la POO en GoStructs como clasesDefinición de métodos en structsReceptores de puntero vs. de valorInicialización de structsFunciones constructorasResumen - Calculadora simple4Interfaces
Introducción a las interfacesImplementación implícitaLa interfaz como contratoInterfaz vacía (any)Type AssertionType SwitchComposición de interfacesInterfaces Stringer y ErrorResumen: Calculadora de figuras7Encapsulamiento
Campos exportados vs. no exportadosEncapsulamiento a nivel de paqueteMétodos Getter y SetterOcultamiento de información en GoResumen - Registros de estudiantes10Genéricos (Go 1.18+)
Introducción a los GenéricosParámetros de tipoRestricciones de tipoStructs genéricosSolución alternativa para métodos genéricosResumen - Colección genérica13Patrones de diseño - Parte 1
Introducción a los patrones de diseñoPatrón SingletonPatrón FactoryPatrón Abstract FactoryPatrón ObserverPatrón Strategy2Inmersión profunda en Tipos y Structs
Tipos básicos y compuestosDefiniciones de tipos personalizadosEtiquetas de StructsStructs anónimosStructs anidadosValores cero y por defectoRepaso - Libreta de contactos5Composición sobre herencia
Por qué Go no tiene herenciaConceptos básicos de incrustación de structsPromoción de métodosIncrustación de múltiples structsIncrustación vs AgregaciónShadowing de métodos incrustadosResumen - Jerarquía de empleados8Manejo de Errores y OOP
La interfaz errorTipos de errores personalizadosEnvoltura de errores (fmt.Errorf)Errores centinelaerrors.Is() y errors.As()Panic, Defer y RecoverResumen - Analizador de archivos3Punteros y Memoria
Fundamentos de punteros en GoPunteros a StructsPaso por valor vs referenciaLa función new()Garbage Collection en GoResumen - Constructor de Linked List6Polimorfismo en Go
Polimorfismo mediante interfacesDuck Typing en GoReglas de satisfacción de interfacesColecciones polimórficasInyección de dependenciasResumen - Procesador de pagos9Concurrencia y POO
Fundamentos de GoroutinesChannels y comunicaciónChannels con buffer vs. sin bufferSentencia Selectsync.Mutex y sync.RWMutexsync.WaitGroupDiseño de estructuras Thread-SafeResumen - Worker Pool12Conceptos avanzados de POO
Patrón Functional OptionsPatrón Builder en GoEncadenamiento de métodosAlias de tipos vs. definicionesFundamentos de ReflectionDescripción general de generación de código