Resumen

Cuando trabajas con interfaces en Go, es común encontrarte con structs que no implementan un contrato de la forma esperada. El patrón de diseño Adapter resuelve exactamente ese problema: actúa como un puente entre una interface definida y un struct cuya implementación difiere, sin necesidad de reescribir código existente ni modificar contratos ya establecidos.

¿Cómo funciona el patrón Adapter en Go?

El Adapter es un patrón de diseño estructural. Su propósito es crear una capa intermedia que adapte el comportamiento de un struct para que cumpla con la firma de una interface, incluso cuando originalmente no lo hace. Esto evita tener que:

  • Crear nuevas interfaces o métodos duplicados.
  • Hacer refactor innecesario de código que ya funciona correctamente.
  • Escribir funciones de procesamiento específicas para cada implementación diferente.

Todo comienza con una interface llamada Payment que exige implementar un método pay sin parámetros ni valores de retorno [01:00]:

go type Payment interface { pay() }

Después se crea un struct CashPayment que implementa esta interface correctamente [01:22]:

go type CashPayment struct{}

func (CashPayment) pay() { fmt.Println("paying using cash") }

La función processPayment recibe cualquier cosa de tipo Payment y llama a pay [01:52]:

go func processPayment(p Payment) { p.pay() }

¿Qué ocurre cuando un struct no cumple la interface?

El conflicto aparece al crear un nuevo struct llamado BankPayment que necesita obligatoriamente un número de cuenta para funcionar [02:10]:

go type BankPayment struct{}

func (BankPayment) pay(bankAccount int) { fmt.Printf("paying using bank account %d\n", bankAccount) }

Este struct no implementa la interface Payment porque su método pay recibe un parámetro int, mientras que la interface exige pay() sin parámetros. Al intentar pasar BankPayment a processPayment, Go lanza un error de compilación [03:15].

Aquí surge la pregunta clave: ¿se debe crear un nuevo processPayment específico? ¿Modificar la interface? Ninguna de esas opciones es ideal.

¿Cómo se construye el adaptador paso a paso?

La solución es crear un adaptador que envuelva al struct incompatible y exponga el método pay tal como la interface lo requiere [03:50]:

go type BankPaymentAdapter struct { BankPayment BankPayment BankAccount int }

func (bpa BankPaymentAdapter) pay() { bpa.BankPayment.pay(bpa.BankAccount) }

El BankPaymentAdapter contiene dos campos:

  • BankPayment: la instancia del struct original.
  • BankAccount: el número de cuenta que BankPayment necesita.

Dentro del método pay() del adaptador, se llama internamente a bpa.BankPayment.pay(bpa.BankAccount), pasándole el parámetro requerido. Desde fuera, el adaptador cumple perfectamente con la interface Payment porque su pay no recibe parámetros [04:20].

¿Cómo se usa el adaptador en la práctica?

Se instancia el adaptador con los datos necesarios y se pasa a processPayment sin ningún error [05:00]:

go func main() { bpa := BankPaymentAdapter{ BankAccount: 5, BankPayment: BankPayment{}, } processPayment(bpa) }

Al ejecutar el programa, la salida confirma que el adaptador funciona correctamente: paying using bank account 5 [05:25].

¿Qué responsabilidad tiene el adaptador?

Toda la lógica de transformación recae exclusivamente en el adaptador. El struct original (BankPayment) no se modifica, la interface (Payment) permanece intacta y la función de procesamiento sigue siendo genérica. El adaptador actúa como un transformador que traduce una firma incompatible a la firma esperada [05:40].

Este enfoque es especialmente valioso cuando trabajas con código de terceros o bibliotecas donde no puedes modificar las definiciones existentes. En lugar de forzar cambios en múltiples lugares, un solo struct adaptador resuelve la incompatibilidad de forma limpia y mantenible.

Si has enfrentado situaciones donde tus structs no encajan con interfaces existentes, prueba implementar este patrón y comparte tu experiencia.