Diseño de software

Patrón Adapter de Swift: cómo envolver API heredadas en protocolos

El patrón Adapter convierte la interfaz de una API heredada en un protocolo que espera el código nuevo. Este artículo explica cómo crear un límite de conversión en Swift, facilitar el reemplazo y las pruebas, y diferenciarlo de otros patrones de envoltura.

4 min de lectura
Imagen de portada de Patrón Adapter de Swift: cómo envolver API heredadas en protocolos

¿Alguna vez te has quedado atascado con código de una API heredada que no podías eliminar, pero tampoco usar tal cual?

Es un obstáculo habitual al conectar un módulo de red antiguo con una pantalla nueva.

En resumen, la solución más limpia es envolver la API heredada en un protocolo (interface) y colocar un adaptador entre ambos.

El patrón Adapter inserta un «convertidor» entre dos interfaces incompatibles.

Si lo confundes con patrones similares que envuelven objetos, como Facade, Proxy y Decorator, Comparativa de 4 patrones de envoltura te ayuda a distinguirlos por su propósito.

Hoy explicaré cómo aplicarlo en Swift siguiendo el proceso que viví personalmente.


Tres cosas que aprenderás

Aquí tienes primero un resumen para quienes tienen prisa.

  1. Definir primero como protocolo la forma que necesita el código nuevo
  2. Crear un tipo adaptador que convierta la API heredada para ajustarla a ese protocolo
  3. Hacer que las pantallas y los modelos de vista dependan solo del protocolo, no de la API heredada

Con solo seguir estas tres reglas, el alcance de los cambios se reduce mucho al reemplazar la API completa más adelante.

Después de cambiar a esta estructura, escribir pruebas me resultó mucho más sencillo.


¿Por qué hace falta el patrón Adapter en Swift?

Las API heredadas normalmente no tienen la forma que necesitamos.

Pueden basarse en callbacks, tener parámetros desordenados o devolver tipos ambiguos.

Si el código nuevo se adapta a una API antigua, todo el código se tambalea el día que esa API desaparece.

Por eso colocamos un adaptador en medio.

Supongamos que el módulo antiguo tiene este aspecto.

// Código heredado difícil de tocar API (Basado en callbacks)
class LegacyUserAPI {
    func fetch(id: Int,
               done: @escaping (NSDictionary?) -> Void) {
        // Una llamada de red antigua...
    }
}

Si llevas NSDictionary directamente hasta la pantalla, cambiar esta API después obligará a desmontar también el código de la pantalla.

A simple vista, no es una forma que apetezca conectar.


Cómo envolver una API heredada en un protocolo (3 pasos)

El proceso de envoltura es más sencillo de lo que parece.

Paso 1: define la forma deseada como protocolo.

Primero escribe la forma en que el código nuevo debería poder decir: «Así quiero llamarlo».

// La interfaz limpia que quiere el código nuevo
protocol UserRepository {
    func user(id: Int) async throws -> User
}

Cambié los callbacks por async/await y NSDictionary por un tipo User.

Paso 2: el adaptador ajusta la API heredada a este protocolo.

Encierra todas las conversiones engorrosas dentro del adaptador.

// Un adaptador que convierte la API heredada al protocolo nuevo
struct LegacyUserAdapter: UserRepository {
    let legacy = LegacyUserAPI()
    func user(id: Int) async throws -> User {
        try await withCheckedThrowingContinuation { cont in
            legacy.fetch(id: id) { dict in
                cont.resume(returning: User(dict))
            }
        }
    }
}

Aquí, withCheckedThrowingContinuation, que convierte callbacks en async, desempeña el papel central.

Paso 3: la pantalla depende solo del protocolo.

El modelo de vista no necesita conocer LegacyUserAPI. Solo necesita conocer UserRepository.

Así, el código heredado aparece en un único lugar: el adaptador.

La API heredada queda oculta tras el adaptador y la pantalla solo ve el protocolo
La API heredada queda oculta tras el adaptador y la pantalla solo ve el protocolo
El código de la pantalla solo necesita conocer este protocolo limpio
El código de la pantalla solo necesita conocer este protocolo limpio

¿Qué cambia al usar un adaptador? (Comparación con llamadas directas)

He comparado en una tabla las llamadas directas con el enfoque del adaptador.

Elemento Llamada directa a la API heredada Envolver con un adaptador
Alcance de cambios al reemplazar la API Toda la pantalla Un solo adaptador
Prueba unitaria Difícil Fácil con un mock
Legibilidad del código nuevo Baja Alta
Trabajo inicial Poco Aumenta un poco

Es cierto que el trabajo inicial aumenta ligeramente.

Pero para mí, este coste mereció totalmente la pena.

Al probar, basta con inyectar un objeto falso que implemente UserRepository para validar la lógica de la pantalla sin red.

Incluso pudimos empezar a desarrollar antes de que estuviera lista la API real.

Si recuerdas esta estructura de tres bloques, ya tienes la mitad hecha
Si recuerdas esta estructura de tres bloques, ya tienes la mitad hecha

Preguntas frecuentes (Q&A)

P. ¿Debo crear el adaptador como struct o como class?

Si no tiene estado, struct es suficiente.

Si necesitas conservar el objeto heredado o compartir referencias, usa class.

P. ¿En qué se diferencia el patrón Adapter del patrón Facade?

El objetivo de Adapter es «hacer compatibles las interfaces».

Facade busca «mostrar varias partes complejas como una sola cosa sencilla», así que la orientación es algo distinta.

P. ¿Cómo debo nombrar el protocolo?

No sigas el nombre heredado; asígnale el rol que necesita el código nuevo.

Por ejemplo, usa UserRepository en lugar de LegacyUserAPI.


No intentes eliminar la API heredada a la fuerza; empieza por aislarla discretamente con un protocolo y un adaptador.

Si confinas el código antiguo en un solo lugar, la siguiente refactorización será mucho más fácil. Prueba a envolver primero la API más desordenada de tu proyecto.

Seguir leyendo