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.

Neste artigo
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.


