O que é SOLID? Os 5 princípios do design orientado a objetos
Entenda os cinco princípios SOLID com exemplos de codigo bom e ruim e aprenda a escrever software flexivel, testavel e facil de manter.

Se você já abriu um arquivo de código, fez uma pequena alteração e, de repente, três outras partes do sistema quebraram, você sentiu na pele o problema que o SOLID tenta resolver. SOLID é um conjunto de cinco princípios de design orientado a objetos que ajudam a escrever software flexível, testável e fácil de evoluir. Neste guia, vamos explicar cada princípio em profundidade, com exemplos de código ruim e bom, os erros mais comuns e como aplicá-los no dia a dia — inclusive quando você está construindo aplicações com a ajuda de IA.
O que é SOLID, afinal?
SOLID é um acrônimo que reúne cinco princípios formulados (ou popularizados) por Robert C. Martin, o "Uncle Bob", a partir do início dos anos 2000. Cada letra representa um princípio:
- S — Single Responsibility Principle (Princípio da Responsabilidade Única)
- O — Open/Closed Principle (Princípio Aberto/Fechado)
- L — Liskov Substitution Principle (Princípio da Substituição de Liskov)
- I — Interface Segregation Principle (Princípio da Segregação de Interfaces)
- D — Dependency Inversion Principle (Princípio da Inversão de Dependência)
O objetivo comum desses princípios é gerenciar dependências. Sistemas mal projetados são rígidos (difíceis de mudar), frágeis (quebram em lugares inesperados) e imóveis (difíceis de reutilizar). SOLID combate exatamente esses três sintomas, organizando o código de forma que mudanças fiquem isoladas e previsíveis (Martin, 2017).
Vale dizer logo de início: SOLID não é uma religião. São diretrizes que orientam decisões de design, não regras absolutas. Aplicá-los cegamente pode gerar abstração demais — o que viola outro princípio importante, o KISS: o princípio Keep It Simple, Stupid no desenvolvimento de software. O bom engenheiro sabe quando aplicar e quando esperar.
Por que SOLID importa?
Antes de mergulhar em cada letra, vale entender o ganho concreto. Código que segue SOLID tende a ser:
- Mais fácil de testar. Componentes pequenos e desacoplados podem ser testados isoladamente, com mocks e stubs simples.
- Mais fácil de manter. Quando cada classe tem um motivo claro para existir, você sabe onde mexer.
- Mais fácil de estender. Adicionar funcionalidades novas sem reescrever o que já funciona reduz o risco de regressões.
- Mais fácil de entender. SOLID anda de mãos dadas com o O que é Clean Code? Guia completo de código limpo: nomes claros, funções curtas e responsabilidades bem definidas.
A relação com IA é direta. Quando você pede para um assistente gerar ou modificar código, módulos pequenos e bem delimitados produzem prompts mais precisos e mudanças mais seguras. Um arquivo de 2000 linhas com dez responsabilidades misturadas confunde tanto o humano quanto a máquina.
Vamos aos princípios.
S — Princípio da Responsabilidade Única (SRP)
Definição
Uma classe deve ter um, e apenas um, motivo para mudar. Martin reformula isso de forma mais precisa: um módulo deve ser responsável por um, e somente um, ator — ou seja, um grupo de pessoas/stakeholders que solicitam mudanças por aquela razão (Martin, 2017). Se duas áreas do negócio diferentes podem forçar alterações na mesma classe, ela tem responsabilidades demais.
Por que importa
Quando uma classe mistura responsabilidades, mudanças em uma delas arriscam quebrar a outra. É o cenário clássico em que "consertar o relatório financeiro" quebra o cálculo de horas trabalhadas, porque ambos viviam na mesma classe Funcionario.
Exemplo ruim
class Relatorio:
def __init__(self, dados):
self.dados = dados
def calcular_totais(self):
# regra de negócio
return sum(item["valor"] for item in self.dados)
def formatar_html(self):
# apresentação
return f"<h1>Total: {self.calcular_totais()}</h1>"
def salvar_em_disco(self, caminho):
# persistência / I/O
with open(caminho, "w") as f:
f.write(self.formatar_html())Essa classe tem três motivos para mudar: a regra de negócio (cálculo), a apresentação (HTML) e a infraestrutura (disco). Três atores diferentes — o time financeiro, o time de design e o time de plataforma — podem solicitar alterações aqui.
Exemplo bom
class CalculadoraDeRelatorio:
def calcular_totais(self, dados):
return sum(item["valor"] for item in dados)
class FormatadorHtml:
def formatar(self, total):
return f"<h1>Total: {total}</h1>"
class ArquivoRepository:
def salvar(self, conteudo, caminho):
with open(caminho, "w") as f:
f.write(conteudo)Agora cada classe tem uma responsabilidade. Mudar o formato do HTML não toca na lógica de cálculo, e vice-versa.
Erros comuns
- Confundir "uma responsabilidade" com "um método". SRP fala de motivos para mudar, não de tamanho. Uma classe pode ter vários métodos coesos servindo ao mesmo ator.
- Explodir o código em microclasses anêmicas. Levar SRP ao extremo gera dezenas de classes que só repassam chamadas. Equilíbrio é essencial.
- Misturar lógica de negócio com I/O. O sinal mais comum de violação é uma classe que calcula e salva no banco e formata a saída.
O — Princípio Aberto/Fechado (OCP)
Definição
Entidades de software (classes, módulos, funções) devem ser abertas para extensão, mas fechadas para modificação. O conceito vem de Bertrand Meyer, que o formulou em Object-Oriented Software Construction (Meyer, 1988). A ideia: você deve conseguir adicionar comportamento novo sem alterar o código existente que já está testado e funcionando.
Por que importa
Toda vez que você edita uma classe estável para acrescentar um caso novo, corre o risco de introduzir bugs no que já funcionava. Se em vez disso você estende — adicionando uma nova classe ou implementação —, o código antigo permanece intocado.
Exemplo ruim
class CalculadoraDeFrete {
calcular(tipo: string, peso: number): number {
if (tipo === "sedex") {
return peso * 2.5;
} else if (tipo === "pac") {
return peso * 1.5;
} else if (tipo === "expressa") {
return peso * 4.0;
}
throw new Error("Tipo desconhecido");
}
}Cada nova transportadora exige editar esse método, adicionando mais um else if. A classe nunca fica "fechada".
Exemplo bom
interface EstrategiaDeFrete {
calcular(peso: number): number;
}
class Sedex implements EstrategiaDeFrete {
calcular(peso: number) { return peso * 2.5; }
}
class Pac implements EstrategiaDeFrete {
calcular(peso: number) { return peso * 1.5; }
}
class CalculadoraDeFrete {
calcular(estrategia: EstrategiaDeFrete, peso: number): number {
return estrategia.calcular(peso);
}
}Para adicionar uma transportadora nova, basta criar uma classe que implemente EstrategiaDeFrete. A CalculadoraDeFrete nunca muda. Esse é, na prática, o padrão Strategy — um dos Design Patterns essenciais que todo dev deveria conhecer e que materializa OCP de forma elegante (Gamma et al., 1994).
Erros comuns
- Antecipar abstrações cedo demais. Criar interfaces para variações que talvez nunca existam é especulação. A boa prática é refatorar para OCP quando o segundo caso aparece, não antes.
- Achar que OCP proíbe qualquer modificação. Corrigir bugs envolve modificar código. OCP fala sobre adicionar comportamento novo sem reescrever lógica estável.
- Cadeias de if/else ou switch por tipo são o sinal clássico de violação de OCP.
L — Princípio da Substituição de Liskov (LSP)
Definição
Objetos de uma superclasse devem poder ser substituídos por objetos de suas subclasses sem quebrar a corretude do programa. O princípio vem de Barbara Liskov: se S é um subtipo de T, então objetos de tipo T podem ser substituídos por objetos de tipo S sem alterar as propriedades desejáveis do programa. Em termos práticos: uma subclasse deve honrar o contrato da classe base.
Por que importa
Herança mal usada cria armadilhas. Quando uma subclasse "mente" sobre o que faz — lança exceções inesperadas, ignora parâmetros ou enfraquece garantias — qualquer código que dependa da classe base pode falhar de forma silenciosa e difícil de rastrear.
O exemplo clássico: Retângulo e Quadrado
class Retangulo:
def __init__(self, largura, altura):
self._largura = largura
self._altura = altura
def set_largura(self, l): self._largura = l
def set_altura(self, a): self._altura = a
def area(self): return self._largura * self._altura
class Quadrado(Retangulo):
def set_largura(self, l):
self._largura = l
self._altura = l # força a invariante quadrado
def set_altura(self, a):
self._largura = a
self._altura = aParece correto: matematicamente, um quadrado é um retângulo. Mas observe este teste, escrito para a classe base:
def testar(r: Retangulo):
r.set_largura(5)
r.set_altura(4)
assert r.area() == 20 # falha se r for um Quadrado!Com um Quadrado, area() retorna 16, não 20. A subclasse violou uma expectativa do contrato. Esse é o famoso problema retângulo/quadrado.
Exemplo bom
Quando a substituição quebra invariantes, herança não é a ferramenta certa. Prefira composição ou abstrações que não façam promessas que as subclasses não conseguem cumprir:
from abc import ABC, abstractmethod
class Forma(ABC):
@abstractmethod
def area(self) -> float: ...
class Retangulo(Forma):
def __init__(self, largura, altura):
self.largura, self.altura = largura, altura
def area(self): return self.largura * self.altura
class Quadrado(Forma):
def __init__(self, lado):
self.lado = lado
def area(self): return self.lado ** 2Agora Retangulo e Quadrado são formas independentes. Nenhuma promete um comportamento que a outra não consiga honrar.
Erros comuns
- Subclasse que lança NotImplementedError em um método herdado. Isso sinaliza que a hierarquia está errada.
- Fortalecer pré-condições ou enfraquecer pós-condições. Uma subclasse não pode exigir mais nem entregar menos do que a base prometeu.
- Usar herança só para reaproveitar código. Herança modela "é um tipo de"; reaproveitamento de código costuma ser melhor resolvido com composição. Esse tipo de ajuste estrutural é tema de O que é refatoração e quando aplicar.
I — Princípio da Segregação de Interfaces (ISP)
Definição
Nenhum cliente deve ser forçado a depender de métodos que não usa. Em vez de uma interface grande e genérica ("interface gorda"), prefira várias interfaces pequenas e específicas para cada cliente.
Por que importa
Quando uma interface tem métodos demais, todos os que a implementam carregam o peso de implementar coisas que não fazem sentido para eles — muitas vezes com pass ou exceções. Além disso, mudanças em métodos que você nem usa podem forçar recompilação ou quebrar seu código.
Exemplo ruim
interface Maquina {
imprimir(doc: string): void;
escanear(doc: string): void;
enviarFax(doc: string): void;
}
class ImpressoraSimples implements Maquina {
imprimir(doc: string) { /* ok */ }
escanear(doc: string) {
throw new Error("Não suportado"); // forçada a implementar
}
enviarFax(doc: string) {
throw new Error("Não suportado"); // forçada a implementar
}
}A ImpressoraSimples só imprime, mas é obrigada a fingir que escaneia e envia fax. Qualquer cliente que receba um Maquina pode chamar enviarFax e quebrar.
Exemplo bom
interface Impressora {
imprimir(doc: string): void;
}
interface Scanner {
escanear(doc: string): void;
}
interface Fax {
enviarFax(doc: string): void;
}
class ImpressoraSimples implements Impressora {
imprimir(doc: string) { /* ok */ }
}
class Multifuncional implements Impressora, Scanner, Fax {
imprimir(doc: string) { /* ok */ }
escanear(doc: string) { /* ok */ }
enviarFax(doc: string) { /* ok */ }
}Cada classe implementa apenas o que realmente faz. Clientes dependem só da capacidade de que precisam.
Erros comuns
- Interfaces "Deus" com dezenas de métodos servindo a clientes muito diferentes.
- Métodos vazios ou que lançam exceção numa implementação são um sintoma direto de violação de ISP — e geralmente também de LSP.
- Confundir ISP com SRP. São primos: SRP fala da responsabilidade de uma classe; ISP fala da forma como os clientes consomem essa classe via interfaces. Manter ambos coesos evita a duplicação que o princípio O que é DRY? Não se repita e elimine duplicação de código combate, sem criar acoplamentos artificiais.
D — Princípio da Inversão de Dependência (DIP)
Definição
O DIP tem duas partes:
- Módulos de alto nível não devem depender de módulos de baixo nível. Ambos devem depender de abstrações.
- Abstrações não devem depender de detalhes. Detalhes devem depender de abstrações.
Em outras palavras: sua lógica de negócio não deve depender diretamente de detalhes concretos como "PostgreSQL", "SendGrid" ou "AWS S3". Ela deve depender de interfaces, e os detalhes concretos as implementam. É o princípio que sustenta arquiteturas em camadas e a Clean Architecture (Martin, 2017).
Por que importa
Sem DIP, sua regra de negócio fica acoplada à infraestrutura. Trocar o banco de dados, o provedor de e-mail ou escrever um teste vira um pesadelo, porque tudo está amarrado a classes concretas. Com DIP, você troca a implementação sem tocar na lógica.
Exemplo ruim
class GatewayMySql:
def salvar_pedido(self, pedido):
# conecta direto no MySQL
...
class ServicoDePedidos:
def __init__(self):
self.db = GatewayMySql() # dependência concreta
def finalizar(self, pedido):
self.db.salvar_pedido(pedido)ServicoDePedidos (alto nível) depende diretamente de GatewayMySql (baixo nível). Para testar finalizar, você precisa de um MySQL real. Trocar para PostgreSQL exige editar o serviço.
Exemplo bom
from abc import ABC, abstractmethod
class RepositorioDePedidos(ABC):
@abstractmethod
def salvar(self, pedido): ...
class ServicoDePedidos:
def __init__(self, repo: RepositorioDePedidos):
self.repo = repo # depende da abstração
def finalizar(self, pedido):
self.repo.salvar(pedido)
# Detalhes concretos implementam a abstração:
class RepositorioMySql(RepositorioDePedidos):
def salvar(self, pedido): ...
class RepositorioEmMemoria(RepositorioDePedidos): # ótimo para testes
def __init__(self): self.pedidos = []
def salvar(self, pedido): self.pedidos.append(pedido)Agora ServicoDePedidos depende apenas da interface RepositorioDePedidos. Em produção você injeta RepositorioMySql; nos testes, RepositorioEmMemoria. A dependência foi "invertida": o detalhe aponta para a abstração, não o contrário.
DIP e injeção de dependência
DIP frequentemente é confundido com injeção de dependência (DI). São coisas relacionadas, mas distintas: DIP é o princípio (depender de abstrações); DI é uma técnica (passar as dependências de fora, geralmente pelo construtor) para satisfazer esse princípio. No exemplo acima, o __init__ recebendo o repositório é injeção de dependência.
Erros comuns
- Instanciar dependências com new dentro da classe que as usa. Isso amarra o alto nível ao baixo nível.
- Criar abstrações que vazam detalhes. Uma interface chamada RepositorioMySql com métodos como executarSqlBruto não inverteu nada — só renomeou o acoplamento.
- Inverter tudo. Nem toda dependência precisa de abstração. Tipos estáveis da linguagem padrão (String, List) não mudam; criar interfaces para eles é overengineering.
Como SOLID conversa com outros princípios
SOLID não vive isolado. Ele faz parte de um ecossistema de boas práticas que se reforçam:
- Clean Code dá a base de legibilidade sobre a qual SOLID atua. Não adianta separar responsabilidades se os nomes e funções continuam confusos.
- DRY evita duplicação; SRP e ISP ajudam a colocar cada regra em um único lugar lógico, o que naturalmente reduz repetição.
- KISS é o contrapeso de SOLID. Quando você sente a tentação de criar cinco interfaces para um caso trivial, KISS lembra que simplicidade ganha de abstração especulativa.
- Design Patterns são, em boa parte, SOLID aplicado. Strategy materializa OCP; Adapter ajuda a respeitar DIP; e muitos outros combinam vários princípios (Gamma et al., 1994).
- Refatoração é o processo pelo qual você migra código legado em direção a SOLID, em pequenos passos seguros e testados.
A maturidade está em saber dosar. Aplicar SOLID é uma decisão de design feita com o contexto em mãos, não um checklist a ser cumprido linha a linha.
SOLID na prática com IA
Ao construir aplicações com assistentes de IA, SOLID se torna ainda mais valioso. Alguns pontos práticos:
- Peça módulos pequenos. Ao gerar código, solicite que cada classe ou função tenha uma responsabilidade clara. Isso facilita revisar e validar o que a IA produziu.
- Defina interfaces antes das implementações. Descrever o contrato (DIP/ISP) e pedir que a IA implemente contra ele gera código mais desacoplado e testável.
- Use os exemplos ruim-vs-bom deste artigo como prompt. Mostrar à IA o padrão que você quer evitar e o que quer alcançar costuma render resultados muito melhores.
- Revise com olhos de SOLID. Ao ler código gerado, pergunte: essa classe tem mais de um motivo para mudar? Esse if/else por tipo deveria ser polimorfismo? Essa dependência concreta deveria ser uma abstração?
A IA acelera a escrita, mas o julgamento de design continua sendo seu. SOLID é parte desse julgamento.
Um resumo rápido dos cinco princípios
| Letra | Princípio | Em uma frase | |------|-----------|--------------| | S | Responsabilidade Única | Uma classe, um motivo para mudar. | | O | Aberto/Fechado | Estenda comportamento sem modificar o existente. | | L | Substituição de Liskov | Subtipos devem honrar o contrato da base. | | I | Segregação de Interfaces | Interfaces pequenas e específicas por cliente. | | D | Inversão de Dependência | Dependa de abstrações, não de detalhes. |
Conclusão
SOLID é, no fundo, uma forma disciplinada de gerenciar dependências para que seu software resista à mudança. SRP isola motivos de mudança; OCP permite crescer sem reescrever; LSP garante que herança não traia expectativas; ISP mantém os contratos enxutos; e DIP protege sua lógica de negócio dos detalhes de infraestrutura. Aplicados com bom senso — e equilibrados com KISS para evitar abstração excessiva —, esses cinco princípios transformam código rígido e frágil em algo flexível, testável e agradável de manter.
Não trate SOLID como dogma. Trate como um conjunto de lentes que você aplica ao revisar e refatorar código, seu ou gerado por IA. Comece percebendo as violações mais óbvias — o switch gigante por tipo, a classe que faz tudo, a dependência concreta cravada no meio da regra de negócio — e refatore um passo de cada vez. Com a prática, esses princípios deixam de ser regras a memorizar e viram intuição de design.
Referências
- Bertrand Meyer (1988). Object-Oriented Software Construction. Prentice Hall.
- Robert C. Martin (2017). Clean Architecture: A Craftsman's Guide to Software Structure and Design. Prentice Hall.
- Gamma, Helm, Johnson, Vlissides (1994). Design Patterns: Elements of Reusable Object-Oriented Software. Addison-Wesley.


