Inyección de dependencias
Parte de la sección Programación Orientada a Objetos del Journey de GO de Coddy — lección 45 de 107.
La Inyección de Dependencias es una técnica en la que un struct recibe sus dependencias desde el exterior en lugar de crearlas internamente. En Go, las interfaces hacen que este patrón sea natural y potente.
En lugar de codificar de forma rígida una implementación específica dentro de una estructura, se acepta una interfaz. Esto permite intercambiar implementaciones sin cambiar el código de la estructura:
type Notifier interface {
Send(message string) string
}
type EmailNotifier struct{}
func (e EmailNotifier) Send(message string) string {
return "Email: " + message
}
type SMSNotifier struct{}
func (s SMSNotifier) Send(message string) string {
return "SMS: " + message
}
Ahora crea una estructura que dependa de la interfaz, no de un tipo concreto:
type OrderService struct {
notifier Notifier // dependencia inyectada a través de la interfaz
}
func NewOrderService(n Notifier) *OrderService {
return &OrderService{notifier: n}
}
func (o *OrderService) PlaceOrder(item string) string {
return o.notifier.Send("Order placed: " + item)
}
El OrderService no sabe ni le importa si está usando correo electrónico o SMS. Inyectas la dependencia al crear el servicio:
func main() {
emailService := NewOrderService(EmailNotifier{})
fmt.Println(emailService.PlaceOrder("Book")) // Email: Pedido realizado: Book
smsService := NewOrderService(SMSNotifier{})
fmt.Println(smsService.PlaceOrder("Phone")) // SMS: Pedido realizado: Phone
}
Este enfoque hace que tu código sea más flexible y fácil de probar. Durante las pruebas, puedes inyectar un notificador simulado (mock) que no envíe mensajes realmente. En producción, inyectas la implementación real. El struct permanece sin cambios en ambos escenarios.
Desafío
FácilVamos a construir un sistema de registro que demuestre el poder de la inyección de dependencias. Crearás un servicio que puede escribir registros en diferentes destinos, sin que el servicio sepa o le importe a dónde van realmente esos registros.
Organizarás tu código en tres archivos:
logger.go: Define una interfazLoggerque requiera un métodoLog(message string) string. Luego crea dos implementaciones de logger diferentes:ConsoleLoggercon un campoPrefix; su métodoLogdevuelve[CONSOLE] [Prefix]: [message]FileLoggercon un campoFilename; su métodoLogdevuelve[FILE:[Filename]] [message]
service.go: Crea un structAppServiceque dependa de la interfazLogger(no de un tipo concreto). Incluye una función constructoraNewAppServiceque acepte unLoggery devuelva un puntero aAppService. Dale aAppServiceun método llamadoDoWork(task string) stringque utilice el logger inyectado para registrar el mensajeProcessing: [task]y devuelva lo que sea que el logger devuelva.main.go: Lee la configuración de la entrada, crea ambos tipos de logger, inyecta cada uno en instancias separadas deAppServicey llama aDoWorken cada servicio con la tarea proporcionada. Imprime el resultado de cada llamada al servicio.
Se proporcionarán las siguientes entradas:
- Línea 1: Prefijo del logger de consola
- Línea 2: Nombre de archivo del logger de archivo
- Línea 3: Tarea a procesar
Por ejemplo, dados INFO, app.log y user authentication, tu salida debería ser:
[CONSOLE] INFO: Processing: user authentication
[FILE:app.log] Processing: user authenticationObserva cómo AppService no sabe si está registrando en una consola o en un archivo; simplemente llama a Log en cualquier logger que se haya inyectado. Esta flexibilidad es la esencia de la inyección de dependencias: el mismo código de servicio funciona con implementaciones de registro completamente diferentes.
Hoja de referencia
La Inyección de Dependencias es una técnica en la que una estructura recibe sus dependencias desde el exterior en lugar de crearlas internamente. En Go, las interfaces hacen que este patrón sea natural y potente.
Define una interfaz para la dependencia:
type Notifier interface {
Send(message string) string
}
Crea implementaciones concretas de la interfaz:
type EmailNotifier struct{}
func (e EmailNotifier) Send(message string) string {
return "Email: " + message
}
type SMSNotifier struct{}
func (s SMSNotifier) Send(message string) string {
return "SMS: " + message
}
Crea una estructura que dependa de la interfaz, no de un tipo concreto:
type OrderService struct {
notifier Notifier // dependencia inyectada a través de la interfaz
}
func NewOrderService(n Notifier) *OrderService {
return &OrderService{notifier: n}
}
func (o *OrderService) PlaceOrder(item string) string {
return o.notifier.Send("Order placed: " + item)
}
Inyecta la dependencia al crear el servicio:
emailService := NewOrderService(EmailNotifier{})
fmt.Println(emailService.PlaceOrder("Book")) // Email: Order placed: Book
smsService := NewOrderService(SMSNotifier{})
fmt.Println(smsService.PlaceOrder("Phone")) // SMS: Order placed: Phone
Este enfoque hace que el código sea más flexible y fácil de probar al permitir que se intercambien diferentes implementaciones sin cambiar el código de la estructura.
Pruébalo tú mismo
package main
import (
"bufio"
"fmt"
"os"
)
func main() {
reader := bufio.NewReader(os.Stdin)
// Leer el prefijo del registrador de consola
prefix, _ := reader.ReadString('\n')
prefix = prefix[:len(prefix)-1]
// Leer el nombre del archivo del registrador de archivos
filename, _ := reader.ReadString('\n')
filename = filename[:len(filename)-1]
// Leer la tarea a procesar
task, _ := reader.ReadString('\n')
if len(task) > 0 && task[len(task)-1] == '\n' {
task = task[:len(task)-1]
}
// TODO: Crear un ConsoleLogger con el prefijo
// TODO: Crear un FileLogger con el nombre de archivo
// TODO: Crear un AppService con el ConsoleLogger inyectado
// TODO: Crear otro AppService con el FileLogger inyectado
// TODO: Llamar a DoWork en cada servicio con la tarea e imprimir los resultados
fmt.Println("result1")
fmt.Println("result2")
}
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 Pool