Há alguns anos revisei um PR de uma feature que recebia um webhook, validava o payload e salvava um registro no banco de dados. Tarefa de uma tarde. O PR tinha quatorze arquivos. Uma interface IPayloadValidator, outra IPayloadValidatorFactory, um IRepository<T> genérico, um IUnitOfWork, três classes que implementavam essas interfaces com exatamente uma instância cada uma em todo o codebase, e um container de injeção de dependências configurado para resolver tudo isso em runtime.
Perguntei ao autor por que tanta estrutura para algo tão pequeno. A resposta foi: "Single Responsibility, Open/Closed, Dependency Inversion". E aí ficou claro o problema. Não havia uma análise do que ia mudar, nem de quem ia estender o quê. Havia uma checklist de princípios SOLID aplicada como um ritual, sem se perguntar se o problema realmente precisava deles.
Esse PR terminou com mais linhas de infraestrutura do que de lógica de negócio. E quando, seis meses depois, foi preciso mudar algo real, a mudança afetou os quatorze arquivos do mesmo jeito, porque as abstrações estavam mal definidas: elas anteciparam um tipo de mudança que nunca chegou e não protegiam contra a que realmente chegou.
Isso não é um caso isolado. É um padrão que qualquer engenheiro sênior reconhece assim que o descrevem: código que segue a letra do SOLID e trai seu espírito. Os princípios SOLID nasceram para resolver um problema real de design orientado a objetos, mas na prática são ensinados e aplicados como regras absolutas, não como heurísticas que dependem do contexto. E essa diferença é o que separa um sistema sustentável de um que parece profissional, mas é uma dor de cabeça.
Single responsibility não significa uma classe por verbo
A interpretação mais comum e mais prejudicial do Single Responsibility é "uma classe deve fazer só uma coisa", entendida da forma mais literal possível: uma classe que valida, outra que transforma, outra que persiste. O resultado é um fluxo de dados que salta entre sete objetos para fazer algo que cabia perfeitamente em um método de vinte linhas.
A definição original de Robert Martin é outra: uma classe deveria ter um único motivo para mudar, ligado a um ator ou a uma necessidade do negócio. Isso é diferente de "uma única ação". Um serviço que valida e persiste um pedido pode ter um único motivo para mudar (as regras de negócio do pedido) mesmo que tecnicamente execute duas etapas. Separá-lo em OrderValidator e OrderPersister não reduz o acoplamento real, só o distribui em mais arquivos.
O custo disso não é estético. Cada camada extra é um salto de contexto para quem lê o código depois. Quando alguém novo entra em um serviço e precisa abrir cinco arquivos para entender um fluxo linear, não está diante de um design limpo. Está diante de uma indireção disfarçada de boa prática.
Open/closed como desculpa para abstrair sem necessidade
Open/Closed diz que o código deveria estar aberto para extensão e fechado para modificação. É um princípio útil quando já se viram duas ou três variações reais de um comportamento e é preciso adicionar uma quarta sem quebrar as anteriores. É um princípio nocivo quando se aplica antes de essa variação existir.
Isso tem nome: abstração prematura. Kent Beck resume melhor do que qualquer princípio: faça funcionar, faça direito, depois faça rápido, e no meio disso, não antecipe estrutura que o problema não pediu. A regra prática que funciona melhor do que o Open/Closed aplicado às cegas é a regra dos três: na primeira vez que se escreve algo, escreve-se direto. Na segunda vez que se repete com uma variação, duplica-se sem culpa. Na terceira vez, aí sim, extrai-se a abstração, porque já existe evidência real do que varia e do que não varia.
Interface segregation quando a interface nunca teve um segundo implementador
O princípio de segregação de interfaces estabelece que nenhuma classe deveria depender de métodos que não usa. Faz sentido em sistemas grandes com múltiplos consumidores de uma mesma interface com necessidades diferentes. O problema é que ele é usado como justificativa para criar uma interface toda vez que existe uma classe, mesmo que essa interface tenha um único implementador desde o primeiro dia e provavelmente para sempre.
Se existe PaymentGateway e se cria IPaymentGateway só porque "é boa prática ter interfaces", não se está segregando nada. Está se adicionando um nível de indireção que nem o compilador precisa nem o ser humano agradece. Em linguagens com tipagem estrutural ou com bom suporte a mocking no nível de classe, essa interface não habilita nenhum teste que já não existisse. Só faz com que ir para a definição no editor leve a um contrato vazio antes de chegar na implementação real.
Interface Segregation importa quando a dor já existe: quando uma classe implementa uma interface de doze métodos e usa só três, forçada a fazer stub do resto com exceções de "não implementado". Aí sim, dividir a interface em partes coerentes é uma melhoria real. Criar a divisão antes de a dor existir é resolver um problema que não existe.
Dependency inversion não é sinônimo de injetar tudo
Dependency Inversion estabelece que os módulos de alto nível não deveriam depender de detalhes de baixo nível, e sim de abstrações. É a base das arquiteturas hexagonais e de portas e adaptadores, e nesses contextos é indispensável: um domínio que não depende do banco de dados nem do framework HTTP é um domínio que pode ser testado e movido.
Mas, no dia a dia, "dependency inversion" virou sinônimo de "tudo se injeta através de uma interface, sempre, sem exceção". Isso gera código em que uma função que chama Math.random() acaba atrás de um IRandomProvider injetado pelo container, porque em algum lugar alguém leu que dependências externas devem ser abstraídas. O resultado é um grafo de dependências impossível de acompanhar a olho nu, com mais cerimônia de configuração do que lógica real, para um sistema que, na prática, nunca vai trocar de provedor de números aleatórios.
A pergunta que separa a inversão útil da inversão decorativa é simples: existe hoje, ou há evidência concreta de que vai existir, uma segunda implementação real dessa dependência? Se a resposta é "talvez algum dia", a abstração é uma aposta, não um design. E as apostas de arquitetura quase sempre são pagas por outro time, anos depois, tentando entender por que existem três camadas entre o controller e uma chamada a uma API externa.
Onde o SOLID de fato compensa a abstração que exige
Nada disso significa que o SOLID esteja mal concebido. Significa que sua aplicação tem custo, e esse custo só se justifica em contextos específicos. Liskov Substitution, por exemplo, é difícil de violar por acidente no dia a dia, mas quando é violado (uma subclasse que lança exceções em métodos que a classe base garante que funcionam) o bug que produz é dos mais difíceis de rastrear, porque o compilador não detecta e o código "parece" correto.
Dependency Inversion vale o custo quando o módulo em questão tem múltiplas implementações reais e comprovadas: um sistema de pagamentos que suporta Stripe e Mercado Pago hoje, não que "poderia suportar" amanhã. Nesse caso a interface não é decoração, é a única forma sã de o domínio não saber nada sobre nenhum dos dois provedores.
Open/Closed vale o custo no núcleo de um produto que muda com frequência previsível: um motor de regras de precificação, um sistema de permissões com papéis que são adicionados a cada trimestre. Nesse caso sim vale a pena projetar para extensão, porque a extensão não é hipotética, é a operação normal do sistema.
Single Responsibility, bem entendido (um motivo de mudança, não uma ação), é quase sempre gratuito de aplicar e raramente tem contraindicações reais. O problema nunca foi esse princípio, foi sua má interpretação como "atomizar tudo".
Os princípios SOLID são um vocabulário de critério, não uma checklist
O que separa um engenheiro sênior de alguém que decorou o SOLID para a entrevista não é saber recitar as cinco letras. É saber quando o custo da abstração é menor do que o de não ter ela, e quando é o contrário. Isso não vem de um acrônimo, vem de ter visto sistemas falharem nas duas direções: por serem rígidos demais e por serem superdimensionados.
Aplicar o SOLID ao pé da letra, sem avaliar se o problema concreto exige isso, não é disciplina de engenharia. É terceirizar o julgamento para uma regra mnemônica e chamar isso de boa prática. Um code review que exige interfaces, injeção de dependências e separação de camadas em um caso que não precisa disso não melhora o design. Está complicando o uso adequado do vocabulário técnico.
O SOLID serve como linguagem compartilhada para discutir trade-offs de design, e como conjunto de heurísticas para quando o sistema já mostrou sinais reais de precisar de flexibilidade. Não serve como substituto do critério, e quando é usado assim, o resultado não é uma arquitetura melhor. É a mesma complexidade de sempre, com nomes melhores.




