Artigo · Blog
O padrão que você não vai refatorar porque está funcionando
Decisões técnicas ruins raramente explodem. Elas apenas ficam. Este artigo é sobre o tipo de código que sobrevive não porque é bom, mas porque funciona o suficiente para nunca virar prioridade.
O padrão que você não vai refatorar porque está funcionando
Numa fintech onde trabalhei, tinha um coordinator que foi escrito em dois dias para resolver um problema de navegação urgente. Era funcional. Estava em produção. Ninguém entendia exatamente o que ele fazia, mas navegar funcionava.
Dois anos depois, esse coordinator ainda estava lá. Com mais trinta chamadas de método adicionadas por cinco pessoas diferentes. Cada uma resolvendo seu próprio problema sem tocar no que já existia.
Não era dívida técnica no sentido clássico. Era algo pior: código que nunca ia ser refatorado porque sempre ia funcionar.
A armadilha do código que nunca quebra
Existe uma categoria de decisão técnica que a indústria quase não discute: a decisão que não falha de forma visível.
Quando algo quebra em produção, você investiga, corrige, às vezes refatora. O ciclo de feedback é curto. O problema tem nome, tem dono, tem resolução.
Mas quando algo funciona mal por anos sem explodir, o ciclo de feedback some. O código fica. O time cresce. Novas pessoas chegam, leem o código, não entendem completamente, mas o comportamento está certo. Então elas adicionam mais uma camada em cima.
Esse é o padrão que mata sistemas aos poucos: não o código ruim que falha, mas o código ruim que sobrevive.
Por que isso acontece com design patterns específicos
Alguns padrões são particularmente propensos a esse problema.
O Coordinator é um deles. A ideia é boa: separar navegação da lógica de apresentação. O problema é que Coordinator não tem uma fronteira natural de responsabilidade. Você não sente quando ele está crescendo demais até ele ter quinhentas linhas e oito delegates.
O mesmo acontece com Builder quando ele começa a conter lógica condicional de negócio. Com Factory quando começa a importar frameworks que não deviam estar naquela camada. Com Singleton quando vira o lugar padrão para guardar estado global que "é só temporário".
Esses padrões têm algo em comum: eles resolvem um problema real na primeira versão, e aceitam qualquer coisa nas versões seguintes.
O problema real não é o padrão
Eu costumava culpar o padrão quando via esse tipo de degradação. Estava errado.
O padrão não tem culpa. O problema é a ausência de uma fronteira explícita que force a conversa quando o código está crescendo além da intenção original.
Num app iOS com UIKit e Clean Architecture, um ViewModel bem definido tem fronteiras claras: recebe input, expõe output, não sabe nada de UIViewController. Quando alguém tenta colocar lógica de apresentação nele, a própria tipagem resiste. A conversa acontece antes do merge.
Mas um Coordinator genérico não tem essa resistência. Você pode adicionar qualquer coisa que não tem outro lugar óbvio. E sempre tem alguma coisa sem lugar óbvio.
A diferença entre um padrão que sobrevive bem ao tempo e um que se degrada não é a elegância do padrão em si. É se ele tem uma fronteira que gera fricção natural quando alguém tenta violá-la.
O que funciona melhor na prática
A decisão que mais me ajudou foi parar de avaliar padrões pela elegância da ideia e começar a avaliar pela resistência que eles oferecem ao crescimento desordenado.
Quando estava na Compass UOL trabalhando em módulos de signup e edição de perfil, eu e o time adotamos uma regra simples: cada módulo tinha um único ponto de entrada tipado. Qualquer coisa que precisasse atravessar a fronteira do módulo tinha que passar por ali. Se a assinatura desse ponto de entrada começasse a ficar grande, era um sinal explícito de que algo estava errado.
Não foi uma solução elegante. Era uma regra burocrática. Mas ela transformou um problema silencioso num problema visível.
O código que cresce sem resistência nunca vai ser refatorado. O código que gera fricção visível tem chance.
Implementação concreta
Na prática, isso se traduz em algumas decisões arquiteturais específicas.
Primeiro, interfaces explícitas entre camadas com tipos que não são Any e não são dicionários. Quando você passa [String: Any] entre camadas, está criando um espaço onde qualquer coisa pode entrar sem ser detectada.
Segundo, protocolos com responsabilidade única. Um protocolo com dois métodos é mais fácil de defender do que um com oito. Quando ele cresce para oito, você percebe. Quando ele começa com oito, a conversa nunca acontece.
Terceiro, e esse é o menos óbvio: testes que validam a fronteira, não o comportamento interno. Num coordinator de navegação, o teste mais valioso não é testar se a tela A navega para a tela B. É testar que o coordinator não sabe nada sobre o conteúdo de A ou B. Se ele souber, o teste quebra.
Esse último tipo de teste é o que mais me ajudou a manter fronteiras ao longo do tempo. Você escreve uma vez e ele continua vigiando por você.
Trade-offs honestos
Essa abordagem tem um custo real.
Fricção explícita atrasa entregas no curto prazo. Quando o prazo está apertado e alguém precisa de um caminho rápido para passar dados entre módulos, uma fronteira rígida vai irritar o time. Vão existir PRs onde a discussão vai ser "por que não posso simplesmente adicionar esse campo aqui?".
E às vezes a resposta honesta é que a arquitetura está errada, não a pessoa. Fronteiras rígidas no lugar errado são piores do que fronteiras flexíveis no lugar certo.
A decisão de onde colocar a fricção é mais importante do que colocar a fricção em todo lugar.
O que eu faria diferente hoje
Em projetos anteriores, eu documentava a intenção dos padrões, mas não documentava o que o padrão não deveria aceitar.
Hoje eu escrevo isso explicitamente. Não como comentário no código, porque comentário envelhece mal. Escrevo como teste ou como constraint de compilação quando possível.
Se um Coordinator não deve saber sobre o estado interno das telas que coordena, isso pode virar uma regra de lint, um protocolo que esconde o que não deve ser visível, ou um teste que falha se a dependência aparecer.
A intenção que só existe na cabeça de quem escreveu o código não sobrevive à rotatividade de time.
Reflexão final
O código que você vai refatorar quando tiver tempo é o código que está quebrando algo.
O código que vai ficar para sempre é o que funciona o suficiente para nunca ser prioridade. E esse código vai crescer. Vai acumular camadas. Vai ser lido por pessoas que não estavam lá no início.
A pergunta que vale fazer antes de escolher um padrão não é "isso resolve o problema?". É "isso vai ser óbvio quando estiver sendo violado?".
Porque o problema com decisões técnicas ruins raramente é que elas falham. É que elas duram.