Voltar ao blog
SOLIDJavaScriptClean Codearquiteturadesenvolvimento

SOLID: 5 Princípios para Escrever Código Limpo e Escalável

12 de março de 2026 às 19:23 - Por Larissa Santos

SOLID: 5 Princípios para Escrever Código Limpo e Escalável

Se você já abriu um arquivo de código antigo e sentiu aquela mistura de vergonha e confusão, você não está sozinha. Todo desenvolvedor tem esse momento. E muitas vezes, o que está por trás desse código difícil de entender não é falta de conhecimento técnico: é falta de estrutura.

É aí que entram os princípios SOLID.

SOLID é um conjunto de cinco diretrizes para escrever código orientado a objetos que seja mais fácil de manter, estender e entender. Não são regras gravadas em pedra, são princípios que, quando internalizados, mudam a forma como você pensa na hora de organizar seu código.

Neste artigo vamos passar por cada um dos cinco princípios com exemplos práticos em JavaScript. A ideia não é decorar as siglas, é entender o problema que cada uma resolve.


De onde veio o SOLID?

O acrônimo foi popularizado por Robert C. Martin, o Uncle Bob, e reúne cinco princípios que já existiam de forma dispersa na comunidade de engenharia de software. Cada letra representa um princípio:

  • S — Single Responsibility Principle
  • O — Open/Closed Principle
  • L — Liskov Substitution Principle
  • I — Interface Segregation Principle
  • D — Dependency Inversion Principle

Pode parecer intimidador à primeira vista, mas você vai perceber que a maioria desses princípios faz sentido intuitivo quando você vê o problema que eles estão tentando resolver.


S — Single Responsibility Principle

Uma classe deve ter uma única razão para mudar.

Traduzindo para o dia a dia: cada pedaço de código deve ser responsável por uma coisa só. Quando você mistura responsabilidades em um lugar só, qualquer mudança em uma delas pode quebrar as outras. E pior: você nunca sabe exatamente onde mexer quando algo dá errado.

Imagine uma classe que cria um usuário no banco de dados e também envia o e-mail de boas-vindas:

js
// ❌ Fazendo coisas demais no mesmo lugar
class Usuario {
  criar(usuario) {
    // salva no banco
    console.log('Usuário salvo no banco')

    // envia e-mail logo em seguida
    console.log('E-mail de boas-vindas enviado')
  }
}

O problema aqui é que se a lógica de e-mail mudar (troca de provedor, novo template, adição de anexo), você vai precisar mexer na mesma classe que cuida de salvar usuários. São duas responsabilidades diferentes convivendo no mesmo lugar.

A solução é separar:

js
// ✅ Cada classe com uma responsabilidade clara
class UsuarioService {
  criar(usuario) {
    // essa classe só sabe sobre criação de usuário
    console.log('Usuário salvo no banco')
  }
}

class EmailService {
  enviarBoasVindas(usuario) {
    // essa classe só sabe sobre envio de e-mail
    console.log('E-mail de boas-vindas enviado')
  }
}

Agora cada classe tem uma razão clara para existir e uma razão clara para mudar. Se o fluxo de e-mail mudar, você mexe só no EmailService. Se a lógica de persistência mudar, você mexe só no UsuarioService. Simples assim.


O — Open/Closed Principle

O código deve ser aberto para extensão, mas fechado para modificação.

Esse é o princípio que mais causa confusão no começo, então vamos direto ao exemplo.

Imagine que você tem um sistema de pagamentos. Hoje aceita PIX e cartão. Amanhã vão pedir boleto. Depois, PayPal. Se a lógica de processar o pagamento vive em um único lugar com condicionais para cada método, toda nova forma de pagamento exige que você abra aquele arquivo e modifique código que já está funcionando. E mexer em código que já funciona é sempre um risco.

js
// ❌ Toda vez que surgir um novo método de pagamento,
// você vai precisar modificar esta classe
class Checkout {
  finalizar(valor, metodo) {
    if (metodo === 'pix') {
      console.log(`Pagamento via PIX: R$ ${valor}`)
    } else if (metodo === 'cartao') {
      console.log(`Pagamento via Cartão: R$ ${valor}`)
    }
    // e quando vier o boleto? Mais um if aqui...
  }
}

A solução é estruturar o código de forma que você possa adicionar novos comportamentos sem precisar tocar no que já existe:

js
// ✅ Cada método de pagamento é uma classe independente
class PagamentoPix {
  pagar(valor) {
    return `Pagamento via PIX: R$ ${valor}`
  }
}

class PagamentoCartao {
  pagar(valor) {
    return `Pagamento via Cartão: R$ ${valor}`
  }
}

// Checkout não sabe qual método de pagamento está usando,
// só sabe que ele tem um método pagar()
class Checkout {
  constructor(metodoPagamento) {
    this.metodoPagamento = metodoPagamento
  }

  finalizar(valor) {
    return this.metodoPagamento.pagar(valor)
  }
}

// Para adicionar boleto, basta criar uma nova classe
// sem mexer em nada que já existe
class PagamentoBoleto {
  pagar(valor) {
    return `Boleto gerado: R$ ${valor}`
  }
}

Perceba: para adicionar um novo método de pagamento, você cria uma nova classe e pronto. O Checkout não precisa nem saber que o boleto existe.


L — Liskov Substitution Principle

Se B herda de A, você deve conseguir usar B em qualquer lugar que esperaria A, sem quebrar nada.

Esse princípio foi definido por Barbara Liskov nos anos 80, e o nome técnico pode assustar, mas a ideia é direta: se você tem uma herança, a classe filha precisa honrar o contrato da classe pai. Ela pode adicionar comportamentos, mas não pode quebrar os que já existiam.

Um exemplo clássico que viola esse princípio: você tem uma classe Passaro com um método mover(), e resolve criar Pinguim extends Passaro. O pinguim é um pássaro, tecnicamente. Mas se mover() na classe pai implica "voar", o pinguim quebra essa expectativa.

js
// ✅ Cada subclasse honra o contrato da classe pai
// sem surpresas para quem está usando
class Passaro {
  mover() {
    return 'O pássaro está se movendo'
  }
}

class Pardal extends Passaro {
  mover() {
    // pardal voa: comportamento esperado de um pássaro
    return 'O pardal está voando'
  }
}

class Pinguim extends Passaro {
  mover() {
    // pinguim nada: diferente, mas ainda é "mover"
    // o contrato de mover() é honrado sem gerar surpresas
    return 'O pinguim está nadando'
  }
}

// Esta função aceita qualquer Passaro
// e vai funcionar corretamente com qualquer subclasse
function mostrarMovimento(passaro) {
  console.log(passaro.mover())
}

mostrarMovimento(new Pardal()) // "O pardal está voando"
mostrarMovimento(new Pinguim()) // "O pinguim está nadando"

O ponto central aqui é: qualquer código que funciona com um Passaro deve continuar funcionando quando você passar um Pardal ou um Pinguim no lugar. Se alguma subclasse quebra esse comportamento, o princípio está sendo violado.

Na prática, esse princípio é especialmente importante quando você está criando hierarquias de componentes ou serviços que vão ser substituídos ou injetados em outras partes do sistema.


I — Interface Segregation Principle

Ninguém deve ser obrigado a depender de métodos que não usa.

Em JavaScript puro não temos interfaces como em TypeScript ou Java, mas o conceito se aplica igualmente. A ideia é: não force uma classe a implementar comportamentos que não fazem sentido para ela.

Pense em uma impressora. Uma impressora simples imprime. Uma multifuncional imprime e digitaliza. Se você criar um "contrato" que exige os dois comportamentos, a impressora simples vai ser obrigada a ter um método de digitalização que não faz nada, só existe para satisfazer uma regra que não é dela.

js
// ❌ Um objeto "gordo" que força quem não precisa
// a carregar comportamentos que não usa
const impressoraMultifuncional = {
  imprimir(doc) {
    console.log('Imprimindo:', doc)
  },
  digitalizar() {
    console.log('Digitalizando')
  },
  enviarFax() {
    console.log('Enviando fax')
  }
}

// A impressora simples não digitaliza e não manda fax,
// mas está sendo forçada a ter esses métodos

A solução é separar as capacidades em partes menores e combinar só o que faz sentido para cada caso:

js
// ✅ Capacidades separadas, combinadas conforme necessário
const podeImprimir = {
  imprimir(doc) {
    console.log('Imprimindo:', doc)
  }
}

const podeDigitalizar = {
  digitalizar() {
    console.log('Digitalizando documento')
  }
}

// Impressora simples: só imprime
class ImpressoraSimples {}
Object.assign(ImpressoraSimples.prototype, podeImprimir)

// Multifuncional: imprime e digitaliza
class ImpressoraMultifuncional {}
Object.assign(ImpressoraMultifuncional.prototype, podeImprimir, podeDigitalizar)

Cada classe recebe exatamente o que precisa, sem carregar peso extra. Em TypeScript, isso fica ainda mais explícito com interfaces separadas, mas o raciocínio é o mesmo.


D — Dependency Inversion Principle

Dependa de abstrações, não de implementações concretas.

Esse é provavelmente o princípio com maior impacto prático no dia a dia de desenvolvimento frontend, especialmente quando você começa a lidar com injeção de dependência, testes e troca de implementações.

O problema acontece quando uma parte do seu código está fortemente acoplada a outra. Se o ServicoDeEmail chama diretamente o SendGrid, qualquer dia que você quiser trocar de provedor, vai precisar abrir o ServicoDeEmail e modificar código que deveria estar funcionando bem.

js
// ❌ Acoplamento direto com uma implementação específica
class ServicoDeEmail {
  constructor() {
    // ServicoDeEmail agora depende diretamente do Sendgrid
    // Quer trocar de provedor? Vai precisar mexer aqui
    this.provedor = new Sendgrid()
  }

  enviar(mensagem) {
    this.provedor.send(mensagem)
  }
}

A solução é inverter essa dependência: em vez de o ServicoDeEmail criar o provedor, o provedor é passado de fora. O serviço apenas espera receber algo que sabe enviar e-mail, sem se importar com qual implementação é essa:

js
// ✅ O serviço depende de uma abstração,
// não sabe (nem precisa saber) qual provedor está usando
class SendgridGateway {
  enviar(mensagem) {
    return `Enviado via SendGrid: ${mensagem}`
  }
}

class MailgunGateway {
  enviar(mensagem) {
    return `Enviado via Mailgun: ${mensagem}`
  }
}

class ServicoDeEmail {
  constructor(provedor) {
    // recebe o provedor de fora, não cria ele mesmo
    this.provedor = provedor
  }

  enviar(mensagem) {
    return this.provedor.enviar(mensagem)
  }
}

// Para trocar de provedor, só muda o que você passa aqui
const servico = new ServicoDeEmail(new SendgridGateway())
// ou:
const servicoAlternativo = new ServicoDeEmail(new MailgunGateway())

Além de facilitar a troca de implementação, esse padrão torna os testes muito mais fáceis: você pode passar um provedor falso nos testes sem precisar configurar nada externo.


SOLID na prática: o que muda no seu código

Ler sobre os princípios é uma coisa. A mudança real acontece quando você começa a reconhecer as violações no código que já existe, e nos que você está escrevendo agora.

Algumas perguntas que ajudam a identificar onde os princípios estão sendo ignorados:

  • Quando uma classe tem métodos que parecem não ter nada a ver entre si, provavelmente está violando o S.
  • Quando você precisa abrir vários arquivos para adicionar um novo comportamento, o O pode estar sendo ignorado.
  • Quando uma subclasse precisa "anular" métodos do pai para funcionar, o L está em risco.
  • Quando você vê uma classe com métodos vazios que existem "só para satisfazer a estrutura", o I está sendo violado.
  • E quando um módulo cria diretamente os objetos de que depende em vez de recebê-los, o D está sendo ignorado.

SOLID não é uma checklist que você marca antes de dar merge. É uma forma de pensar na organização do código que, com o tempo, passa a ser natural.


Conclusão

Os princípios SOLID não existem para complicar o código ou aumentar o número de arquivos. Eles existem para tornar o código mais honesto: cada parte faz o que diz que faz, depende só do que realmente precisa e pode ser estendida sem medo de quebrar o que já funciona.

Você não precisa aplicar tudo de uma vez. Comece pelo S: identifique uma classe no seu projeto que está fazendo coisas demais e separe as responsabilidades. Quando isso virar natural, os outros princípios começam a fazer mais sentido na sequência.

O melhor código não é o que usa mais padrões, é o que você consegue abrir seis meses depois e entender sem precisar de coragem.

Referências

Feito com e Vue.js
2026 © Larissa Santos