Princípios SOLID na prática
Os cinco princípios SOLID explicados com exemplo de código simples, sem jargão de teoria de engenharia de software. Entenda o problema real que cada um resolve e quando vale aplicar.
SOLID é um daqueles termos que aparecem citados em quase toda conversa sobre código limpo, mas que pouca gente para pra entender de fato, geralmente porque a explicação vem cheia de jargão de teoria de engenharia de software antes de qualquer exemplo prático. São cinco princípios, cada letra da sigla representando um deles, e o objetivo comum entre todos é o mesmo: deixar o código mais fácil de mudar sem quebrar o resto do sistema.
S: Single Responsibility (responsabilidade única)
Uma classe ou função deveria ter um motivo só pra mudar. Se uma função calcula o valor de um pedido, envia e-mail de confirmação e salva no banco, ela tem três motivos diferentes pra ser alterada: mudou a regra de cálculo, mudou o texto do e-mail, mudou a estrutura do banco. Qualquer uma dessas mudanças obriga mexer na mesma função, aumentando o risco de quebrar algo que não tinha nada a ver com a alteração.
// ruim: uma função fazendo três coisas diferentes
function finalizarPedido(pedido) {
const total = pedido.itens.reduce((soma, item) => soma + item.preco, 0);
enviarEmail(pedido.usuario.email, `Pedido confirmado: R$${total}`);
salvarNoBanco(pedido, total);
}
// melhor: cada função com um motivo só de mudar
function calcularTotal(pedido) {
return pedido.itens.reduce((soma, item) => soma + item.preco, 0);
}
function confirmarPedido(pedido) {
const total = calcularTotal(pedido);
enviarEmail(pedido.usuario.email, `Pedido confirmado: R$${total}`);
salvarNoBanco(pedido, total);
}Separar não elimina a dependência entre as partes, mas isola cada responsabilidade, então testar calcularTotal sozinho não exige simular envio de e-mail nem banco de dados.
O: Open/Closed (aberto pra extensão, fechado pra modificação)
Um código deveria permitir adicionar comportamento novo sem precisar alterar o que já existe e já funciona. O exemplo clássico é um cálculo de desconto que cresce com if empilhado:
// ruim: toda regra nova exige mexer nessa mesma função
function calcularDesconto(tipoCliente, valor) {
if (tipoCliente === "vip") return valor * 0.2;
if (tipoCliente === "novo") return valor * 0.1;
if (tipoCliente === "funcionario") return valor * 0.5;
return 0;
}Toda vez que aparece um tipo de cliente novo, essa função precisa ser editada de novo, correndo o risco de quebrar uma regra que já funcionava. Uma estrutura que permite estender sem alterar o que já existe:
const regrasDesconto = {
vip: (valor) => valor * 0.2,
novo: (valor) => valor * 0.1,
funcionario: (valor) => valor * 0.5,
};
function calcularDesconto(tipoCliente, valor) {
const regra = regrasDesconto[tipoCliente];
return regra ? regra(valor) : 0;
}Um tipo de cliente novo vira uma entrada nova no objeto, sem tocar na função que já está em produção e já foi testada.
L: Liskov Substitution (substituição de Liskov)
Se uma parte do código espera um tipo geral, qualquer versão mais específica dele deveria funcionar sem quebrar nada. O exemplo mais citado é um Quadrado herdando de Retangulo, mudando largura e altura juntos pra manter os lados iguais:
class Retangulo {
setLargura(l) { this.largura = l; }
setAltura(a) { this.altura = a; }
}
class Quadrado extends Retangulo {
setLargura(l) { this.largura = l; this.altura = l; }
setAltura(a) { this.altura = a; this.largura = a; }
}
function testar(retangulo) {
retangulo.setLargura(5);
retangulo.setAltura(10);
console.log(retangulo.largura * retangulo.altura); // espera 50
}
testar(new Quadrado()); // devolve 100, quebra a expectativaQuem escreveu testar espera que Retangulo (e qualquer coisa que herde dele) se comporte como um retângulo de verdade, mas Quadrado muda esse comportamento de um jeito que quebra a expectativa de quem só conhece a classe base. O princípio não é sobre herança em si, é sobre garantir que uma versão mais específica de algo não traia o comportamento esperado da versão geral.
I: Interface Segregation (segregação de interface)
Ninguém deveria ser forçado a implementar um método que não faz sentido pra ele. Imagina uma interface Funcionario com bater_ponto() e receber_comissao(), aplicada tanto em funcionário CLT quanto em vendedor freelancer:
interface Funcionario {
baterPonto(): void;
receberComissao(): void;
}
class Freelancer implements Funcionario {
baterPonto() {
throw new Error("freelancer não bate ponto");
}
receberComissao() { /* ... */ }
}O Freelancer é obrigado a implementar baterPonto, mesmo isso não fazendo sentido nenhum pra ele. O princípio sugere quebrar isso em interfaces menores e mais específicas, e cada classe implementa só o que de fato se aplica a ela:
interface RegistraPonto {
baterPonto(): void;
}
interface RecebeComissao {
receberComissao(): void;
}
class Freelancer implements RecebeComissao {
receberComissao() { /* ... */ }
}D: Dependency Inversion (inversão de dependência)
Código de alto nível não deveria depender diretamente de detalhe de implementação de baixo nível, os dois deveriam depender de uma abstração em comum. Na prática, isso costuma aparecer como receber uma dependência de fora em vez de criar ela por dentro:
// ruim: a função de negócio já sabe que o banco é Postgres
function salvarUsuario(usuario) {
const pg = new PostgresClient();
pg.insert("usuarios", usuario);
}
// melhor: a dependência entra de fora, não é criada aqui dentro
function salvarUsuario(usuario, banco) {
banco.insert("usuarios", usuario);
}Na segunda versão, salvarUsuario não sabe (nem precisa saber) se banco é Postgres, MySQL ou um objeto falso usado só em teste. Trocar de banco de dados, ou testar essa função sem um banco de verdade rodando, fica bem mais simples porque a dependência concreta não está amarrada dentro da função.
Fechando
Nenhum desses cinco princípios é uma regra pra seguir de forma absoluta em todo lugar, forçar SOLID num script pequeno de uso único é overengineering desnecessário. Eles fazem diferença de verdade em código que vai crescer e ser alterado com frequência, onde o custo de uma responsabilidade misturada, um if empilhado ou uma dependência amarrada aparece meses depois, na hora que alguém precisa mudar uma parte sem arriscar quebrar outra que não tinha nada a ver.
Leia também
- GraphQL vs REST: quando cada um faz sentido15 de setembro de 2026 · 4 min
- Migrations de banco de dados: como versionar o schema sem quebrar produção14 de setembro de 2026 · 4 min
- Record Skill: o novo jeito de criar skills no Claude13 de setembro de 2026 · 5 min
- Webhooks: o que são e como validar com segurança12 de setembro de 2026 · 4 min