01 - SOLID
video: https://youtu.be/... no frontmatter da nota.SOLID
SOLID é um conjunto de cinco princípios de design orientado a objetos: Single
Responsibility, Open-Closed, Liskov Substitution, Interface Segregation e Dependency
Inversion. A sigla foi consolidada por Michael Feathers a partir de regras que Robert C.
Martin observou se repetir em bases de código que sobreviviam a anos de evolução. Em
produção, os princípios funcionam como critérios objetivos de coesão e acoplamento: eles
orientam onde ficam as fronteiras de módulo e quem pode depender de quem. Um código alinhado
a SOLID concentra cada motivo de mudança em um lugar previsível, o que reduz o raio de
impacto de um defeito e o custo de cada release. O valor arquitetural aparece quando o
sistema já atende usuários reais e precisa evoluir sem parada para reescrita, porque nesse
momento a qualidade das fronteiras determina a velocidade do time.
O problema que o conjunto resolve
Sistemas sem critérios de fronteira acumulam módulos que precisam ser alterados em conjunto
a cada demanda. O sintoma clássico é a mudança simples que obriga a editar dez arquivos e
reexecutar toda a suíte de testes, porque regra de negócio, acesso a banco e notificação
vivem na mesma classe. Esse acoplamento não nasce no primeiro deploy; ele se forma em cada
entrega apressada em que a saída mais curta foi acrescentar mais um if ao serviço central. O
efeito acumulado é dívida arquitetural: o custo de cada feature nova cresce enquanto a
confiança no processo de entrega cai. SOLID ataca esse processo em nível de classe e de
módulo, com regras explícitas sobre responsabilidade, extensão, substituição, segregação de
interfaces e direção de dependência. Aplicado com bom senso, o conjunto transforma decisões
de design em conversa objetiva sobre risco, e não em preferência pessoal.
SRP: responsabilidade única como driver de coesão
O princípio da responsabilidade única afirma que um módulo deve ter um, e apenas um, motivo
para mudar. A leitura arquitetural é sobre coesão: agrupar o que muda junto e separar o que
muda em ritmos diferentes. Regras de cobrança mudam por decisão de produto, consultas de
banco mudam por decisão de performance e templates de e-mail mudam por decisão de marketing;
quando as três vivem em uma única FaturamentoService, três interesses competem pelo mesmo
arquivo. A violação aparece em produção como conflitos recorrentes de merge, testes que
quebram por mudanças alheias e revisões de código lentas. O teste prático é descrever a
responsabilidade da classe em uma frase sem conjunções como "e" ou "também"; se a frase
exige o "e", existem duas preocupações disputando o mesmo módulo. No nível de arquitetura, o
SRP escala para contextos delimitados: cada contexto expõe apenas o vocabulário e os casos
de uso do próprio domínio.
OCP: extensão sem modificação
O princípio aberto-fechado pede módulos abertos para extensão e fechados para modificação:
novos comportamentos entram como código novo, e não como edição de código já validado em
produção. O mecanismo típico é o polimorfismo, com estratégias ou plugins registrados em um
ponto de despacho estável. Em um gateway de pagamento, incluir um novo adquirente não
deveria exigir alterar o fluxo de captura que já funciona; deveria exigir criar um adaptador
e registrá-lo. O ganho em produção é mensurável: o arquivo crítico deixa de receber patch a
cada release, e o raio de impacto de uma feature fica restrito ao módulo novo. O custo é
antecipar corretamente o ponto de variação; ponto de extensão errado vira indireção sem
benefício, por isso o OCP raramente compensa no primeiro caso concreto.
LSP: substituição de Liskov
O princípio de substituição de Liskov exige que um subtipo possa ser usado no lugar do
supertipo sem quebrar o contrato esperado pelo cliente. A violação clássica é a subclasse
que lança exceção em operações herdadas, enfraquece pré-condições ou fortalece
pós-condições; o chamador passa a precisar de checagens defensivas, e o polimorfismo deixa
de funcionar. Em produção, esse cheiro aparece como instanceof espalhado pelo código, sinal
de que a hierarquia mente sobre o próprio contrato. O exemplo pedagógico é o par retângulo e
quadrado, mas os casos reais são o repositório que herda save() sem suportar gravação em
lote, ou o cliente HTTP simulado que responde com latência zero e mascara problemas de
timeout. Respeitar o LSP mantém honesto o contrato das interfaces, que é a base de qualquer
fronteira arquitetural.
ISP: segregação de interfaces
O princípio da segregação de interfaces pede que clientes não dependam de métodos que não
usam. Interfaces gordas, como um UserRepository com trinta métodos, obrigam todo consumidor
a conhecer a superfície inteira e toda implementação a dar conta de tudo. O efeito prático é
acoplamento disfarçado: uma alteração em um método que a maioria não usa força rebuild e
reteste de todos os clientes. A mitigação é fatiar a interface pelo papel do consumidor: o
serviço de autenticação precisa de findByEmail, o serviço de auditoria precisa de
listChangedSince, e nenhum dos dois precisa do contrato completo. Em arquitetura de
microserviços, a mesma ideia aparece nas APIs públicas: uma fachada enxuta por caso de uso
reduz a chance de consumidores dependerem de operações instáveis.
DIP: inversão de dependência e arquitetura hexagonal
O princípio da inversão de dependência afirma que módulos de alto nível, que carregam as
regras de negócio, não devem depender de detalhes de baixo nível, como drivers de banco e
clientes HTTP; ambos devem depender de abstrações. A consequência arquitetural é a inversão
da seta de dependência: a infraestrutura implementa interfaces definidas pelo domínio, e não
o contrário. Esse é o fundamento da arquitetura hexagonal e de sua formulação em ports and
adapters: o núcleo publica portas, e adaptadores de banco, fila e HTTP se conectam a elas.
Em produção, o ganho aparece em testes de integração substituíveis, na troca de provedor de
mensageria sem tocar nas regras de negócio e na capacidade de compilar e testar o domínio
sem infraestrutura. A direção da dependência é a decisão arquitetural; a interface em si é
apenas o mecanismo.
Aplicação prática: finalização de pedido
# Porta definida pelo núcleo do domínio (DIP): a regra de negócio declara
# o que precisa, sem conhecer o provedor concreto de mensageria.
class NotificadorPedido(Protocol):
def confirmar(self, pedido: "Pedido") -> None: ...
# Caso de uso coeso (SRP): apenas orquestra o pedido; sem SQL, sem HTTP
# e sem template de e-mail dentro desta classe.
class FinalizarPedido:
def __init__(self, repo: "RepositorioPedidos", notificador: NotificadorPedido):
self.repo = repo
self.notificador = notificador
def executar(self, pedido: "Pedido") -> None:
pedido.validar() # regra central do domínio
self.repo.salvar(pedido) # persistência escondida atrás da porta
# Despacho por estratégia (OCP): um canal novo entra como classe
# nova registrada, sem alterar este fluxo já validado.
self.notificador.confirmar(pedido)
# Adaptador de infraestrutura: implementa a porta e depende do domínio;
# o caminho inverso nunca deve acontecer.
class NotificadorSNS:
def confirmar(self, pedido: "Pedido") -> None:
publicar_topico("pedidos-confirmados", pedido.id) # detalhe de infra
O módulo de composição, ou composition root, é o único lugar autorizado a conhecer classes
concretas: ele instancia FinalizarPedido com um RepositorioPedidosPostgres e um
NotificadorSNS, e o restante do código enxerga apenas portas. Esse padrão mantém o grafo de
dependências explícito, centraliza a configuração de ambiente e simplifica tanto os testes
quanto a migração de provedor. Em sistemas multi-tenant, a estratégia de notificação pode
sair de configuração por tenant, e o caso de uso permanece intacto; essa é a prova prática
de que as fronteiras foram bem traçadas.
O excesso de SOLID e abstrações desnecessárias
Aplicar SOLID como dogma produz indireção sem valor. Cadeias de DTO, mapper, factory e
interface de implementação única para uma operação simples aumentam o custo de leitura sem
reduzir risco real. Regras de bolso úteis: abstrair a partir do segundo caso concreto,
manter uma interface quando ela serve de fronteira de teste e evitar ponto de extensão sem
consumidor. A pergunta de triagem é qual mudança futura concreta a abstração antecipa e qual
o custo de introduzi-la depois; se a resposta é vaga, o código direto de hoje é a escolha
certa. Abstração é investimento, e investimento exige retorno esperado. Times que tratam
cada letra do acrônimo como obrigação terminam com camadas que apenas transferem dados de um
lado para o outro.
Erros comuns e boas práticas
- Confundir SRP com classe pequena: uma classe de quarenta métodos pode ter um único motivo de mudança, e uma de dez linhas pode ter dois; o critério correto é motivo de mudança, não tamanho.
- Criar interface para toda classe: a interface com implementação única só vale quando marca fronteira de teste ou ponto real de variação já identificado.
- Usar herança para reuso de código: viola LSP com frequência; prefira composição quando não existe contrato genuinamente substituível.
- Acumular pontos de extensão especulativos: cada hook sem consumidor é custo de manutenção e fonte de confusão para quem chega novo no time.
- Aplicar SOLID dentro do serviço e ignorar fronteiras externas: contratos entre serviços exigem versionamento e compatibilidade retroativa, e interfaces internas bonitas não resolvem isso.
- Espalhar o wiring pela aplicação: centralize a montagem de dependências no composition root, o único lugar que deve conhecer implementações concretas.
Sinais de violação em produção
A tabela a seguir funciona como checklist de revisão arquitetural: mapeie o sintoma,
confirme a violação no código e aplique a mitigação correspondente.
| Princípio | Violação típica | Sintoma em produção | Mitigação |
|---|---|---|---|
| SRP | Serviço que valida, persiste e notifica | Conflitos de merge e testes instáveis | Extrair contextos por motivo de mudança |
| OCP | Switch de adquirentes no fluxo central | Patch no mesmo arquivo a cada release | Estratégias registradas em despacho estável |
| LSP | Subclasse que lança NotImplementedError | instanceof acumulado no chamador | Redesenhar contrato ou usar composição |
| ISP | Interface com dezenas de métodos | Rebuild de clientes alheios à mudança | Fatiar a interface pelo papel do consumidor |
| DIP | Domínio importando SDK de nuvem | Troca de provedor exige reescrita | Ports and adapters com injeção de dependência |