Design de software

Padrão Adapter em Swift: como envolver APIs legadas em protocolos

O padrão Adapter converte a interface de uma API legada em um protocolo esperado pelo código novo. Este guia mostra como criar uma fronteira de conversão em Swift, facilitar substituições e testes e diferenciá-lo de outros padrões de encapsulamento.

4 min de leitura
Imagem de capa de Padrão Adapter em Swift: como envolver APIs legadas em protocolos

Você já ficou preso a um código de API legada que não podia apagar, mas também não conseguia usar do jeito que estava?

É um obstáculo comum ao conectar um módulo de rede antigo a uma tela nova.

Indo direto ao ponto, a solução mais limpa é envolver a API legada em um protocolo (interface) e colocar um adaptador entre os dois.

O padrão Adapter insere um “conversor” entre duas interfaces incompatíveis.

Se você confunde esse padrão com Facade, Proxy e Decorator, que também envolvem objetos, Comparação de 4 padrões de encapsulamento ajuda a diferenciá-los pelo objetivo.

Hoje vou mostrar como aplicar isso em Swift, seguindo o fluxo que vivenciei na prática.


Três coisas que você vai aprender

Para quem está com pressa, aqui vai um resumo primeiro.

  1. Definir primeiro, em um protocolo, o formato que o código novo precisa
  2. Criar um tipo adaptador que converta a API legada para esse protocolo
  3. Fazer telas e view models dependerem apenas do protocolo, não da API legada

Seguindo só essas três regras, o escopo das alterações diminui muito quando você precisar substituir a API inteira.

Depois de mudar para essa estrutura, escrever testes ficou muito mais fácil para mim.


Por que precisamos do padrão Adapter em Swift?

As APIs legadas geralmente não têm o formato que queremos.

Elas podem ser baseadas em callbacks, ter parâmetros confusos ou retornar tipos ambíguos.

Se o código novo se curva às limitações de uma API antiga, a base inteira treme no dia em que essa API desaparece.

Por isso colocamos um adaptador no meio.

Vamos supor que o módulo antigo seja parecido com isto.

// Código legado difícil de mexer API (Baseado em callback)
class LegacyUserAPI {
    func fetch(id: Int,
               done: @escaping (NSDictionary?) -> Void) {
        // Uma chamada de rede antiga...
    }
}

Se você levar NSDictionary diretamente até a tela, trocar essa API depois exigirá desmontar também o código da tela.

Dá para ver que não é algo que você queira conectar diretamente.


Como envolver uma API legada em um protocolo (3 etapas)

O processo de encapsulamento é mais simples do que parece.

Etapa 1: defina o formato desejado em um protocolo.

Primeiro, descreva o formato que permite ao código novo dizer: “É assim que quero chamar”.

// A interface limpa que o código novo precisa
protocol UserRepository {
    func user(id: Int) async throws -> User
}

Troquei callbacks por async/await e NSDictionary por um tipo User.

Etapa 2: o adaptador ajusta a API legada a este protocolo.

Deixe todas as conversões complicadas confinadas ao adaptador.

// Um adaptador que converte a API legada para o novo protocolo
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))
            }
        }
    }
}

O withCheckedThrowingContinuation que converte callbacks em async exerce aqui o papel principal.

Etapa 3: faça a tela depender apenas do protocolo.

O view model não precisa conhecer LegacyUserAPI. Basta conhecer UserRepository.

Assim, o código legado aparece em um único lugar: o adaptador.

A API legada fica escondida atrás do adaptador, e a tela vê apenas o protocolo
A API legada fica escondida atrás do adaptador, e a tela vê apenas o protocolo
O código da tela só precisa conhecer este protocolo limpo
O código da tela só precisa conhecer este protocolo limpo

O que muda ao usar um adaptador? (Comparação com chamadas diretas)

Comparei em uma tabela as chamadas diretas com a abordagem do adaptador.

Item Chamada direta da API legada Envolver com um adaptador
Escopo das alterações ao substituir a API Tela inteira Um único adaptador
Teste unitário Difícil Fácil com mock
Legibilidade do código novo Baixa Alta
Esforço inicial Baixo Aumenta um pouco

É verdade que o esforço inicial aumenta um pouco.

Mas, para mim, esse custo valeu totalmente a pena.

Nos testes, basta injetar um objeto falso que implemente UserRepository para validar a lógica da tela sem rede.

Conseguimos começar o desenvolvimento mesmo antes de a API real ficar pronta.

Lembre desta estrutura de três blocos e você já terá feito metade do trabalho
Lembre desta estrutura de três blocos e você já terá feito metade do trabalho

Perguntas frequentes (Q&A)

P. Devo criar o adaptador como struct ou class?

Se não houver estado, struct é suficiente.

Se precisar manter o objeto legado ou compartilhar referências, use class.

P. Qual é a diferença entre os padrões Adapter e Facade?

O objetivo do Adapter é “compatibilizar interfaces”.

O Facade busca “apresentar várias partes complexas como uma coisa simples”, então a direção é um pouco diferente.

P. Como devo nomear o protocolo?

Não siga o nome legado; escolha o nome com base no papel que o código novo precisa.

Por exemplo, use UserRepository em vez de LegacyUserAPI.


Não tente apagar a API legada à força; comece isolando-a discretamente com um protocolo e um adaptador.

Ao confinar o código antigo a um único lugar, a próxima refatoração fica muito mais fácil. Experimente envolver primeiro a API mais complicada do seu projeto.

Continue lendo