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 if e switch que 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.

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ípioSinal que justifica aplicarRisco se aplicado sem o sinal
SRPA classe muda por dois motivos diferentes, vindos de responsáveis diferentesEspalhamento de lógica sem redução real de acoplamento
OCPJá existe mais de uma extensão concreta do mesmo ponto de variaçãoAbstração especulativa que engessa o formato errado
LSPExiste herança real com risco de substituição quebrando contratoPouco risco de aplicar, grande risco de ignorar
ISPImplementações ignoram métodos ou lançam “não suportado”Fragmentação que esconde o que o objeto realmente faz
DIPTeste exige substituição real ou há mais de uma implementação vivaInterfaces-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)
  • Database e Mailer sã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.