Princípios SOLID explicados com exemplos: quando ajudam e quando viram dogma
Os 5 princípios SOLID com código real, explicados sem enrolação — e com honestidade sobre quando geram overengineering em vez de resolver problemas.
Você já viu código onde cada operação de banco vive numa classe separada, cada formatter tem sua própria interface, e para entender o que salvarUsuario() faz você precisa navegar por sete arquivos? Provavelmente foi escrito por alguém que acabou de descobrir SOLID.
Os princípios SOLID têm valor real — quando aplicados com julgamento. O problema é que eles são ensinados como regras absolutas e aplicados como checklist, produzindo exatamente o tipo de overengineering que deveriam prevenir.
O que é SOLID e de onde veio
SOLID é um acrônimo para cinco princípios de design orientado a objetos formulados por Robert C. Martin no início dos anos 2000. Se você leu o post sobre Clean Code sem dogmas, já viu esses princípios citados de passagem — aqui vamos destrinchar cada um com código real e com a pergunta que importa: quando esse princípio ajuda e quando ele vira burocracia?
Os cinco são:
- S — Single Responsibility Principle
- O — Open/Closed Principle
- L — Liskov Substitution Principle
- I — Interface Segregation Principle
- D — Dependency Inversion Principle
S — Single Responsibility Principle
Um módulo deve ter uma única razão para mudar.
# Ruim: User faz tudo
class User:
def save(self): ...
def send_welcome_email(self): ...
def generate_report(self): ...
# Melhor: cada classe tem uma responsabilidade
class UserRepository:
def save(self, user): ...
class UserNotifier:
def send_welcome_email(self, user): ...
O SRP é o mais útil dos cinco — e o mais mal interpretado. "Uma responsabilidade" não significa "um método". Uma classe OrderProcessor que valida, calcula impostos e persiste um pedido pode estar cumprindo uma única responsabilidade: processar pedidos. O que você não quer é OrderProcessor enviando e-mail de marketing ou gerando PDF de relatório — isso sim é responsabilidade diferente, com razão diferente para mudar.
Quando vira dogma: quando você quebra uma função de 30 linhas coesa em três funções de 10 linhas que só fazem sentido juntas, porque "cada função deve fazer uma coisa". SRP é sobre coesão de propósito, não sobre contagem de linhas.
O — Open/Closed Principle
Software deve ser aberto para extensão e fechado para modificação.
# Ruim: cada novo formato exige modificar a classe
class ReportExporter:
def export(self, data, format):
if format == "pdf":
...
elif format == "csv":
...
# adicionar "xlsx" exige mexer aqui
# Melhor: novos formatos são extensões
class ReportExporter:
def export(self, data, formatter: ReportFormatter):
return formatter.format(data)
class PdfFormatter(ReportFormatter): ...
class CsvFormatter(ReportFormatter): ...
OCP é poderoso quando você tem um ponto de extensão real — um sistema de plugins, formatos de exportação, provedores de pagamento. O código acima faz sentido se você genuinamente vai adicionar novos formatters regularmente.
Quando vira dogma: quando você cria abstrações para extensões que nunca vão acontecer. "E se no futuro precisarmos de outro formato?" é a pergunta que transforma código simples em hierarquias de classes que ninguém pediu. YAGNI ainda existe.
L — Liskov Substitution Principle
Subclasses devem ser substituíveis pelas suas superclasses sem quebrar o comportamento esperado.
# Violação clássica: Square herda Rectangle mas quebra invariantes
class Rectangle:
def set_width(self, w): self.width = w
def set_height(self, h): self.height = h
def area(self): return self.width * self.height
class Square(Rectangle):
def set_width(self, w):
self.width = w
self.height = w # quebra o comportamento de Rectangle
Se você tem código que funciona com Rectangle e passa Square, ele vai produzir resultados inesperados. O LSP diz que herança deve preservar o contrato da classe pai — não apenas a interface, mas o comportamento esperado.
Quando vira dogma: LSP pressupõe herança. Em linguagens com duck typing (Python, JavaScript, Go) ou em código predominantemente funcional, LSP raramente tem aplicação direta. Criar hierarquias de classes só para poder falar que o LSP está sendo respeitado é burocracia sem benefício.
I — Interface Segregation Principle
Não force uma classe a implementar métodos que ela não usa.
// Ruim: uma interface gorda
interface Worker {
work(): void;
eat(): void;
sleep(): void;
}
// Melhor: interfaces focadas
interface Workable { work(): void; }
interface Feedable { eat(): void; }
class Robot implements Workable {
work() { ... }
// não precisa implementar eat() e sleep()
}
ISP é útil quando você tem implementações que precisam de subconjuntos de funcionalidade. Um Robot que implementa Worker e é forçado a ter um método eat() que não faz nada (ou lança exceção) é um design errado.
Quando vira dogma: quando você cria uma interface para cada método individual porque "interfaces devem ser pequenas". O exemplo acima com Workable e Feedable é razoável. Uma interface Saveable com um único método save() que é implementada por exatamente uma classe e usada em exatamente um lugar é overhead sem motivo.
D — Dependency Inversion Principle
Módulos de alto nível não devem depender de módulos de baixo nível. Ambos devem depender de abstrações.
# Ruim: UserService depende diretamente de PostgreSQL
class UserService:
def __init__(self):
self.db = PostgreSQLConnection() # acoplamento concreto
def find_user(self, id):
return self.db.query(f"SELECT * FROM users WHERE id={id}")
# Melhor: depende da abstração
class UserService:
def __init__(self, repo: UserRepository):
self.repo = repo
def find_user(self, id):
return self.repo.find(id)
Esse é o princípio com melhor ROI dos cinco. Quando UserService depende de UserRepository (abstração), você pode passar PostgreSQLUserRepository em produção e InMemoryUserRepository nos testes. Sem DIP, testar lógica de negócio exige banco rodando — que é exatamente o tipo de fricção que mata a cobertura de testes ao longo do tempo.
Quando vira dogma: quando você cria interfaces com um único implementador que nunca vai mudar. Se EmailService tem uma interface IEmailService implementada apenas por SMTPEmailService e você nunca vai trocar o SMTP por outro mecanismo, a interface existe só para dizer que você "aplicou DIP". O benefício real do DIP é a capacidade de substituição — se não há substituição real, há burocracia real.
O padrão dos cinco
Se você reparou, todos os cinco têm a mesma estrutura: fazem sentido quando há uma variabilidade real que justifica a abstração. Formatos de exportação variáveis — OCP vale. Implementações de banco intercambiáveis — DIP vale. Hierarquia de formas geométricas — LSP vale.
O erro não é aplicar SOLID. O erro é aplicar SOLID preventivamente, antes de a variabilidade existir. Código com abstrações desnecessárias é mais difícil de ler e de modificar do que código direto — exatamente o oposto do objetivo.
Martin Fowler tem uma frase boa sobre isso: abstrações pré-maturas são tão prejudiciais quanto otimizações prematuras. Você está pagando o custo de indireção sem ter o benefício de flexibilidade.
SOLID e Clean Code: a relação real
SOLID e Clean Code não são o mesmo conjunto de ideias — são complementares com focos diferentes. Clean Code trata principalmente de legibilidade no nível de linha e função: nomeação, tamanho, comentários. SOLID trata de design no nível de classe e módulo: responsabilidades, dependências, extensibilidade.
Dá para ter código Clean Code que viola SOLID (uma função de 15 linhas bem nomeada que faz coisas demais) e dá para ter código SOLID que viola Clean Code (uma hierarquia corretamente abstraída com nomes terríveis e sem comentários explicando o design).
A sobreposição real é no SRP — e no DIP quando está no nível de função via injeção de dependência. O resto são camadas diferentes de análise.
Para ver como isso fica na prática em revisões de código — antes e depois de aplicar um princípio — uso o Comparador de Código para colocar as duas versões lado a lado. É mais rápido do que ir e voltar no histórico do Git quando você está explicando uma decisão de design para alguém.
Perguntas frequentes
SOLID ainda vale para linguagens funcionais ou TypeScript moderno?
Parcialmente. SRP e DIP têm equivalentes diretos em código funcional — coesão de módulo e injeção de dependência via parâmetro. OCP mapeie para composição de funções. LSP e ISP são menos relevantes porque herança de classes é menos central. Em TypeScript moderno com composição e tipos estruturais, você frequentemente aplica os princípios sem as hierarquias de classes que os exemplos clássicos usam.
Qual dos cinco princípios tem o maior impacto prático?
DIP, sem dúvida. A capacidade de substituir implementações — especialmente para testes — tem retorno direto na cobertura e na velocidade de desenvolvimento. SRP vem logo depois, mas no nível de módulo/serviço, não de função. OCP, LSP e ISP dependem muito de você realmente ter os pontos de variação que justificam as abstrações.
É possível aplicar todos os cinco ao mesmo tempo sem overengineering?
Sim, mas só quando todos têm justificativa concreta no mesmo design. Um repositório de dados naturalmente pede DIP (abstração de banco), ISP (interfaces separadas para leitura e escrita), e SRP (repositório não faz validação de negócio). Quando os cinco se sobrepõem organicamente, é sinal de que o design tem variabilidade real. Quando você precisa forçar um deles, é sinal de que ele não pertence ali.
SOLID se aplica só a POO?
Foi formulado em contexto de POO, mas os princípios têm análogos em outros paradigmas. Em Go, por exemplo, interfaces pequenas e composição refletem ISP e DIP. Em Python funcional, módulos coesos com injeção de dependência via parâmetro refletem SRP e DIP. O nome e os exemplos clássicos são POO — o princípio subjacente é mais amplo.
SOLID é um mapa, não um destino
Os cinco princípios descrevem sintomas de problemas reais de design: acoplamento alto, rigidez, fragilidade, impossibilidade de testar. Quando você identifica esses sintomas no código, SOLID oferece direções para resolver.
Aplicar SOLID preventivamente — antes dos sintomas aparecerem — é como tomar remédio para uma doença que você ainda não tem. O risco é que a medicação tem efeitos colaterais: complexidade desnecessária, indireção sem benefício, dificuldade de leitura.
A versão útil do SOLID é: conheça os princípios, reconheça os sintomas que cada um resolve, aplique quando os sintomas aparecem. Essa é a diferença entre um engenheiro que usa SOLID e um que segue SOLID.
- 01 Clean Code sem dogmas: o que realmente importa Clean Code virou religião — e tem fiéis que aplicam os mandamentos sem entender a teologia. O que realmente reduz bugs e custo de manutenção.
- 02 Endereço IP: diferença entre IPv4 e IPv6, esgotamento e NAT IPv4 esgotou em 2020 na América Latina. Entenda a notação, os limites do NAT, as mudanças do IPv6 e o que isso significa para quem escreve código.