Tem um tipo de código que é vendido como “código SOLID” e, na prática, é um labirinto. Classe com um método, interface com uma implementação, factory que só repassa parâmetro, camada que existe só para justificar a próxima camada. Você entra no repositório pra corrigir um bug pequeno e precisa abrir sete arquivos para seguir o fluxo.
Esse código não é SOLID bem aplicado. É SOLID aplicado sem dor. Os princípios foram escritos para resolver problemas concretos de manutenção, extensão e teste. Quando você aplica todos, o tempo todo, sem nenhum desses problemas aparecer, o resultado costuma ser o contrário do que os princípios existem para garantir.
A tese deste post é simples: SOLID é um conjunto de diagnósticos, não uma receita. Aplique cada princípio quando a dor correspondente existir, não antes.
O erro não é conhecer SOLID. É aplicar preemptivamente#
Os cinco princípios que formam SOLID — single responsibility, open/closed, Liskov substitution, interface segregation, dependency inversion — vieram de observações sobre código que envelheceu mal. Classe que cresceu demais, hierarquia que quebrou na primeira subclasse nova, módulo que exigia recompilar o mundo para mudar uma regra, interface gorda que forçava implementação vazia, acoplamento direto a detalhe de infraestrutura que travava o teste.
Cada princípio é uma resposta a um sintoma. Não a uma estética.
Quando o time aplica SOLID “no papel”, o fluxo costuma ser o inverso: escreve a interface antes de existir segunda implementação, separa a classe antes de ter segunda responsabilidade real, injeta dependência antes de ter algo para substituir no teste. É engenharia defensiva contra problemas que ainda não apareceram, paga agora com complexidade que já apareceu.
SRP: útil como pergunta, ruim como tesoura automática#
O Single Responsibility Principle funciona bem como pergunta: essa classe tem mais de uma razão para mudar? Se a resposta honesta é sim, separar costuma pagar. Se é não, separar produz dois arquivos que só existem porque alguém achou que “classe grande é ruim”.
O sintoma real que SRP resolve é específico:
- a mesma classe muda toda vez que regra de negócio muda E toda vez que formato de saída muda
- times diferentes mexem no mesmo arquivo por motivos diferentes
- teste de uma parte só roda se você montar o mundo inteiro da classe
Sem esses sintomas, quebrar OrderProcessor em OrderValidator, OrderCalculator, OrderPersister, OrderNotifier e OrderLogger só espalha a mesma lógica em cinco arquivos. O acoplamento não sumiu, mudou de lugar. E agora ninguém acha o fluxo.
A regra operacional é mais conservadora do que a versão popular do princípio: só quebre quando já tem duas razões concretas para mudar. Antes disso, uma classe bem nomeada de 200 linhas é mais fácil de manter do que cinco classes de 40 espalhadas pelo módulo.
OCP: a abstração certa vem depois, não antes#
Open/Closed diz que um módulo deveria estar aberto para extensão e fechado para modificação. O problema é que a maior parte do código que tenta ser OCP “desde o começo” acaba com uma abstração especulativa: a interface foi desenhada para uma segunda implementação que nunca chegou, ou chegou diferente do que a abstração previa.
O caso em que OCP puxa peso é claro:
- você já extendeu o mesmo ponto mais de uma vez
- a extensão nova está entrando em conflito com
ifeswitchque crescem a cada regra nova - o teste regride toda vez que alguém mexe no miolo para adicionar variante
Quando esses sinais aparecem, abstrair o ponto de variação paga. Quando não aparecem, você está escolhendo a forma da abstração no escuro — e forma errada de abstração é mais cara de corrigir do que duplicação honesta.
Uma heurística que funciona: prefira duplicar duas ou três vezes antes de abstrair. A terceira duplicação mostra o eixo real de variação. As duas primeiras costumam mentir.
LSP: o único com regra dura#
Liskov Substitution é o princípio que não tolera interpretação flexível. Se B herda de A, qualquer código que funcionava com A tem que continuar funcionando com B. Ponto.
Quebra clássica: Square extends Rectangle que redefine setWidth para também alterar setHeight. Código que manipula Rectangle e confia em independência de lados quebra silenciosamente quando recebe um Square. O compilador não acusa. O teste antigo passa. O comportamento novo é inconsistente.
O valor prático de LSP não está em decorar o princípio, está em desconfiar de herança que restringe comportamento do pai. Quando a subclasse joga exceção onde o pai não jogava, ignora parâmetro que o pai respeitava, ou muda invariante que o pai garantia, você já quebrou LSP.
A saída costuma ser composição no lugar de herança, ou uma hierarquia mais honesta onde Square e Rectangle são irmãos de uma abstração comum, não pai e filho.
ISP: ajuda quando a interface virou catálogo#
Interface Segregation vale quando uma interface cresceu ao ponto de forçar implementações vazias ou lançar NotImplementedError. Se Repository tem 14 métodos e metade das implementações ignora dez deles, a interface virou catálogo, não contrato.
O sintoma honesto de ISP:
- implementações precisam estampar “não suportado” em método que herdaram
- mudar um método obriga recompilação ou retrabalho em consumidores que nem usam aquele método
- teste de uma funcionalidade exige mockar métodos não relacionados à funcionalidade
Quando esses sinais aparecem, quebrar em interfaces menores paga. Quando não aparecem, ISP aplicado preventivamente fragmenta tanto que o leitor precisa consultar três interfaces para entender o que um objeto faz. Interface pequena demais tem o mesmo problema de classe pequena demais: o fluxo some.
DIP: serve quando o teste ou a substituição cobram#
Dependency Inversion diz para depender de abstração, não de implementação. Na prática do dia a dia, isso costuma virar “use interface e injeção de dependência” — e é aqui que a simplificação machuca.
O princípio é bom. A aplicação mecânica é ruim.
DIP paga quando:
- você quer substituir a implementação no teste sem subir infraestrutura
- existe mais de uma implementação real do contrato (produção, sandbox, fake, in-memory)
- a política do domínio não deveria saber qual banco, fila ou provedor externo está do outro lado
DIP não paga quando:
- há uma implementação única e não vai existir outra num horizonte honesto
- a interface foi criada só para satisfazer o princípio e a única coisa que ela faz é repetir a classe concreta
- o teste funciona bem com a implementação real e injetar abstração só espalha configuração
O sinal de que DIP foi aplicado longe demais é ver IOrderServiceImpl implements IOrderService com uma única implementação, nenhuma intenção de ter outra, e o consumidor recebendo a interface “por via das dúvidas”.
Aplicação dogmática produz os sintomas que SOLID promete evitar#
Quando os cinco princípios são aplicados o tempo todo, sem dor correspondente, alguns padrões aparecem com frequência:
- navegar o código exige abrir muitos arquivos para seguir um fluxo simples
- a maior parte das interfaces tem uma única implementação
- classes pequenas dependem umas das outras em cadeias longas
- teste fica cheio de mock porque tudo virou dependência injetada
- mudar uma regra pequena pede alteração em quatro camadas
Esses são exatamente os sintomas que SOLID, bem aplicado, deveria aliviar: dificuldade de manutenção, teste frágil, mudança que se espalha. Dogma produz o mesmo sofrimento que a ausência total dos princípios, só que com mais cerimônia.
Uma regra operacional: dor primeiro, princípio depois#
Antes de aplicar qualquer princípio, vale perguntar o que ele está resolvendo agora.
| Princípio | Sinal que justifica aplicar | Risco se aplicado sem o sinal |
|---|---|---|
| SRP | A classe muda por dois motivos diferentes, vindos de responsáveis diferentes | Espalhamento de lógica sem redução real de acoplamento |
| OCP | Já existe mais de uma extensão concreta do mesmo ponto de variação | Abstração especulativa que engessa o formato errado |
| LSP | Existe herança real com risco de substituição quebrando contrato | Pouco risco de aplicar, grande risco de ignorar |
| ISP | Implementações ignoram métodos ou lançam “não suportado” | Fragmentação que esconde o que o objeto realmente faz |
| DIP | Teste exige substituição real ou há mais de uma implementação viva | Interfaces-espelho sem propósito, configuração espalhada |
A tabela não é rígida. O objetivo é forçar a pergunta: qual sintoma concreto justifica a mudança? Se não tem resposta, provavelmente o princípio ainda não precisa entrar.
O que SOLID não resolve#
Vale deixar explícito porque muito debate de code review mistura coisas.
SOLID trata de estrutura de classes e dependências dentro de um módulo. Ele não substitui:
- modelagem de domínio correta
- fronteira clara entre módulos e bounded contexts
- decisões de concorrência e consistência
- escolha de estrutura de dados e complexidade algorítmica
- arquitetura de integração entre sistemas
Um sistema pode ser rigorosamente SOLID e ainda assim estar mal modelado, mal particionado e lento. E um sistema pode ter classes grandes, herança parcimoniosa e pouca interface, e ainda assim envelhecer bem, se as fronteiras maiores estiverem certas.
Tratar SOLID como checklist universal de qualidade é confundir microestrutura com arquitetura.
Um pequeno exemplo concreto#
Considere um OrderProcessor que valida, calcula, persiste e notifica. Aplicação dogmática transforma isso em cinco classes, cinco interfaces e um orquestrador. Aplicação por dor pergunta antes:
// Versão direta. Uma razão para mudar: regra de pedido.
class OrderProcessor {
constructor(private db: Database, private mailer: Mailer) {}
async process(order: Order): Promise<void> {
this.validate(order);
const total = this.calculate(order);
await this.db.save({ ...order, total });
await this.mailer.send(order.customerEmail, `Pedido ${order.id} confirmado`);
}
private validate(order: Order): void { /* ... */ }
private calculate(order: Order): number { /* ... */ }
}
Essa classe é SOLID o suficiente enquanto:
- a regra de validação muda pelo mesmo motivo que a regra de cálculo (ambas são regra de pedido)
DatabaseeMailersão abstrações que você já precisa para testar sem subir infra- não existe segunda política de notificação concorrendo com e-mail
O dia em que chega SMS, push e webhook, notify vira ponto de variação real e abstrair paga. O dia em que cálculo de imposto passa a depender de jurisdição, calcular vira responsabilidade própria. Antes desse dia, separar é inventar custo.
Checklist curto para code review#
Quando bater dúvida se uma mudança está aplicando SOLID ou inventando cerimônia, esses filtros ajudam:
- A dor que esse princípio resolve já apareceu nesse código, ou é hipotética?
- A abstração proposta tem mais de uma implementação real prevista no curto prazo?
- O teste fica mais simples ou mais cheio de mock depois da mudança?
- O leitor precisa abrir mais ou menos arquivos para entender o fluxo?
- A regra que vai mudar com mais frequência no próximo trimestre fica mais fácil ou mais difícil de tocar?
Se a maioria das respostas indica que a mudança piora leitura, teste ou agilidade, o princípio está sendo aplicado preventivamente. Recue.
Fechando#
SOLID continua valendo. O problema nunca foi o conteúdo dos princípios, foi a forma como eles viraram dogma: aplicados sempre, aplicados cedo, aplicados sem sintoma. Bem usados, eles são um conjunto de perguntas que você faz ao código quando ele começa a sofrer. Mal usados, eles produzem o mesmo sofrimento que prometiam evitar, só que com nomes mais bonitos.
Se você for levar uma ideia deste post, que seja esta: só aplique o princípio quando a dor correspondente já apareceu. Até lá, prefira código direto, bem nomeado e fácil de abrir.

Comentários
Os comentários usam Disqus e só são carregados se você clicar no botão abaixo.