Home Comunidade Eventos YouTube Networks Desafios // produtos Academy News CTI | CVE Contato

01 - SOLID

#acoplamento#coesao#design-orientado-a-objetos#principios-de-design#solid
video explicativo
Video explicativoEspaco reservado para o video desta anotação — adicione 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