Inyección de Dependencias en Go: para principiantes
La Inyección de Dependencias (DI, por sus siglas en inglés), es uno de los patrones de diseño clave que ayuda a escribir código limpio, comprobable y mantenible. En el mundo de Go, donde la simplicidad y la claridad son primordiales, DI permite evitar la creación de «objetos divinos» y acoplamientos rígidos entre componentes. En este artículo, analizaremos qué es la Inyección de Dependencias en Go, por qué es necesaria y cómo aplicarla en la práctica, utilizando ejemplos simples y fáciles de entender.
¿Qué es la Inyección de Dependencias?
Imagine que está escribiendo un programa que envía correos electrónicos de bienvenida a los usuarios. Su código probablemente se vería así:
package main
import "fmt"
type EmailService struct{}
func (e *EmailService) SendWelcomeEmail(userEmail string) { fmt.Printf("Enviando correo a %s", userEmail)}
type UserController struct { emailService *EmailService}
func NewUserController() *UserController { return &UserController{ emailService: &EmailService{}, // Acoplamiento rígido }}
func (uc *UserController) RegisterUser(email string) { // ... lógica de registro uc.emailService.SendWelcomeEmail(email)}
func main() { controller := NewUserController() controller.RegisterUser("user@example.com")}El problema de este código es que UserController crea la instancia de EmailService por sí mismo. Esto se llama acoplamiento rígido. Si desea reemplazar EmailService por, digamos, SmsService o usar un objeto mock para pruebas, tendrá que modificar el código dentro de UserController.
La Inyección de Dependencias resuelve este problema. En lugar de crear dependencias dentro de la estructura, las «inyectamos» desde el exterior. La idea principal es: «No preguntes lo que necesitas. Di lo que necesitas y recíbelo listo».
Inyección manual de dependencias a través del constructor
La forma más simple y común de DI en Go es pasar las dependencias a través de una función constructora. Reescribamos nuestro ejemplo:
package main
import "fmt"
// Definimos una interfaz para flexibilidadtype Notifier interface { Send(to string, message string) error}
type EmailService struct{}
func (e *EmailService) Send(to string, message string) error { fmt.Printf("Email a %s: %s", to, message) return nil}
type SmsService struct{}
func (s *SmsService) Send(to string, message string) error { fmt.Printf("SMS a %s: %s", to, message) return nil}
type UserController struct { notifier Notifier // Dependencia a través de interfaz}
// NewUserController — constructor que acepta la dependenciafunc NewUserController(n Notifier) *UserController { return &UserController{ notifier: n, }}
func (uc *UserController) RegisterUser(email string) { // ... lógica de negocio uc.notifier.Send(email, "¡Bienvenido!")}
func main() { // Inyectamos la dependencia manualmente emailNotifier := &EmailService{} controller := NewUserController(emailNotifier) controller.RegisterUser("user@example.com")
// Cambiamos fácilmente la implementación smsNotifier := &SmsService{} controller2 := NewUserController(smsNotifier) controller2.RegisterUser("+123456789")}¿Qué cambió?
- Definimos la interfaz
Notifier. AhoraUserControllerno sabe qué servicio se utiliza exactamente — Email o SMS. - El constructor
NewUserControlleraceptaNotifiercomo argumento. Esto es inyección de dependencia. - En
main, nosotros decidimos qué implementación pasar.
Este enfoque hace que el código sea flexible, comprobable y que cumpla con el principio de Inversión de Control.