Pedro Sousa
← Artigos

Artigo · Blog

A interface que você escondeu atrás de um protocolo não desapareceu. Ela só ficou mais difícil de encontrar.

Extrair um protocolo não resolve acoplamento. Na maioria dos casos, ele apenas move a dependência para um lugar onde o compilador para de reclamar. O problema continua lá, só ficou invisível.

A interface que você escondeu atrás de um protocolo não desapareceu. Ela só ficou mais difícil de encontrar.

Numa fintech onde trabalhei, herdei um módulo de fluxo de pagamento com acoplamento explícito em todo lugar. PaymentService chamava UserRepository diretamente. CheckoutViewModel instanciava dependências no próprio init. Era o tipo de código que qualquer sênior olha e sente vontade de refatorar na hora.

A equipe anterior tinha começado uma refatoração. Criaram protocolos para tudo. UserRepositoryProtocol. PaymentServiceProtocol. CheckoutCoordinatorProtocol. O módulo estava cheio de tipos com sufixo Protocol.

O problema: o acoplamento não tinha diminuído. Só estava escondido.


O que a maioria entende por "desacoplamento"

Existe uma crença muito difundida de que extrair uma interface resolve dependência. No mundo iOS, isso se traduz em: criar um protocolo entre dois tipos significa que eles não dependem mais um do outro.

Não é verdade.

Dependência não é sobre qual tipo você referencia. É sobre qual comportamento você assume. Se o seu CheckoutViewModel depende de um PaymentServiceProtocol com 12 métodos, sendo 9 deles específicos do fluxo de pagamento com cartão, você tem uma dependência concreta. O protocolo é só um alias.

A abstração não eliminou o acoplamento. Ela apenas removeu a verificação em tempo de compilação.


O sinal que eu deveria ter percebido antes

No projeto da fintech, o primeiro sinal apareceu nos testes.

Toda vez que adicionávamos um método novo em PaymentServiceProtocol, precisávamos atualizar os mocks em seis arquivos diferentes. Se esquecesse um, o compilador reclamava. Se não esquecesse, o teste passava, mas o mock não fazia nada de útil porque o método novo era ignorado.

O segundo sinal veio quando tentamos substituir a implementação de pagamento por uma versão nova, compatível com um gateway diferente. Esperávamos que o protocolo fosse a fronteira que permitia a troca. Na prática, a nova implementação precisava honrar comportamentos implícitos que nunca estavam documentados na interface, só no código de chamada.

A interface tinha crescido para atender o consumidor, não para definir uma fronteira.

Isso tem nome. Alguns chamam de interface poluída. Eu prefiro chamar de protocolo espelho: uma interface que apenas reflete a implementação concreta que ela deveria substituir.


O problema real por trás do problema

Quando você cria um protocolo orientado ao consumidor, você cria um acoplamento de comportamento disfarçado de abstração.

PaymentServiceProtocol com métodos como processCardPayment, processPixPayment e retryLastTransaction não é uma fronteira. É um contrato específico de implementação com um nome genérico.

Uma fronteira real define o que o consumidor precisa, não o que a implementação oferece.

Se o CheckoutViewModel precisa apenas confirmar uma transação, a interface deveria expressar exatamente isso. Um único método. Sem contexto de cartão, sem contexto de Pix, sem lógica de retry visível para quem chama.

protocol TransactionConfirming {
    func confirm(_ transaction: Transaction) async throws -> TransactionResult
}

O PaymentService implementa esse protocolo. Mas o protocolo não foi criado para mapear o PaymentService. Foi criado para expressar a necessidade do CheckoutViewModel.

A diferença parece sutil. Na prática, é o que separa uma abstração que dura anos de uma que você refatora seis meses depois.


O que funciona melhor: interface orientada ao consumidor

No projeto da fintech, a mudança que fez diferença foi simples de descrever e trabalhosa de executar.

Paramos de perguntar "o que o PaymentService oferece?" e começamos a perguntar "o que cada consumidor realmente precisa?"

O CheckoutViewModel precisava confirmar uma transação. Criamos TransactionConfirming.

O ReceiptViewController precisava buscar o comprovante de uma transação passada. Criamos TransactionReceiptFetching.

O PaymentService implementou os dois protocolos. Mas os protocolos existiam para os consumidores, não para o serviço.

O resultado prático: os mocks ficaram pequenos. Testes pararam de quebrar por métodos que o teste nem usava. A nova implementação do gateway entrou sem reescrever contratos, porque os contratos eram mínimos.


Trade-offs que ninguém menciona

Essa abordagem tem um custo real.

Você vai ter mais protocolos. Vinte consumidores diferentes podem gerar dez ou quinze interfaces pequenas em vez de dois protocolos grandes. Isso exige disciplina para nomear bem e organizar o código de forma que o time entenda a estrutura.

Em times pequenos ou projetos com ciclo de vida curto, a granularidade pode ser overhead. Um protocolo espelho é mais rápido de criar e mais óbvio de entender no primeiro dia.

O problema é que "mais óbvio no primeiro dia" vira "mais difícil de mudar no décimo mês". E na maioria dos projetos que eu vi, o décimo mês sempre chega.


O que eu faria diferente hoje

Quando vejo alguém criando um protocolo, eu faço uma pergunta antes: esse protocolo foi criado para o consumidor ou para o provider?

Se a resposta for o provider, o protocolo provavelmente vai crescer na direção errada.

A outra coisa que eu faria diferente: não deixaria o tamanho do protocolo crescer sem revisão. Um protocolo com mais de quatro ou cinco métodos é um sinal de que ele pode estar servindo contextos demais. Não é regra absoluta, mas é um bom gatilho para perguntar se a fronteira ainda faz sentido.

Na OLX, onde o módulo de Conta tinha vários times escrevendo código em paralelo, a disciplina de interfaces pequenas era o que tornava possível revisar um pull request sem ter que entender metade do sistema. Um protocolo com escopo claro é mais fácil de revisar do que um com escopo indefinido.


O padrão que sobrevive ao tempo

Abstrações que duram não são abstrações que escondem complexidade. São abstrações que definem fronteiras reais.

Um protocolo que cresce junto com a implementação concreta não é uma abstração. É uma sombra.

A diferença entre um design que você refatora em dois anos e um que você consegue estender sem medo não está no número de protocolos. Está em se os protocolos foram criados para comunicar intenção ou para satisfazer o compilador.

Satisfazer o compilador é fácil. Qualquer protocol com um sufixo resolve isso.

Comunicar intenção é a parte que exige pensar no consumidor antes de pensar na implementação.

E essa é a parte que a maioria pula.