4 min de leitura
•
22 de abril de 2025
SOLID é um conjunto de princípios bastante conhecido no mundo da programação orientada a objetos. Apesar de ser requisito em muitas vagas por aí, muita gente ainda sente dificuldade em definir – e, principalmente, aplicar – esses princípios no dia a dia do desenvolvimento.
Para marcar a estreia desse blog, resolvi começar justamente com eles. Afinal, um software considerado "bem-sucedido" vai inevitavelmente precisar ser evoluído e mantido, e é aí que o SOLID brilha. Este conteúdo será dividido em duas partes, e você está lendo a primeira agora.
SOLID é um acrônimo, onde cada letra representa um princípio de boas práticas no desenvolvimento de software:
Nada mais do que isso. Vamos entender cada um deles com calma.
O Princípio da Responsabilidade Única diz que uma classe deve ter apenas um motivo para mudar – ou seja, ela deve ter apenas uma função bem definida dentro do sistema.
Para deixar mais claro, imagine se escrevêssemos um sistema inteiro dentro de um único arquivo. Esse arquivo seria responsável por tudo: entrada de dados, regras de negócio, persistência... ou seja, ele teria várias responsabilidades.
Isso foge totalmente do SRP, e claro, deixaria o código bem mais difícil de manter.
A solução aqui seria literalmente separar esse único arquivo em arquivos menores, com as responsabilidades mais bem definidas – onde, quando fosse necessário, apenas aquele arquivo (ou bloco de código) seria especificamente alterado.
O Princípio Aberto/Fechado sugere que o código deve estar:
Aberto para extensão, mas fechado para modificação.
Ou seja: você deve ser capaz de adicionar novos comportamentos ao sistema sem alterar o que já existe. Isso é possível usando abstrações, como interfaces, herança ou até mesmo funções de ordem superior.
public List<String> findWord(Predicate<String> filterWord) {
return getAllWords().stream()
.filter(filterWord)
.toList();
}
Aplicar esse princípio melhora bastante a manutenibilidade do código e permite maior reaproveitamento com menos retrabalho.
O Princípio da Substituição de Liskov fala sobre herança e como ela deve ser usada corretamente.
Segundo o LSP:
Qualquer classe filha (ou subtipo) deve poder ser usada no lugar da sua classe mãe sem que o funcionamento do sistema seja afetado.
Isso significa que os comportamentos esperados devem ser mantidos — incluindo validações, efeitos colaterais e imutabilidade de variáveis, por exemplo.
interface Carrinho {
double getValor();
int getQuantidade();
}
class CarrinhoUsuario implements Carrinho {
private double valor;
private int quantidade;
CarrinhoUsuario(double valor, int quantidade) {
this.valor = valor;
this.quantidade = quantidade;
}
@Override
public double getValor() {
return valor;
}
@Override
public int getQuantidade() {
return quantidade;
}
}
class CarrinhoNulo implements Carrinho {
@Override
public double getValor() {
return 0;
}
@Override
public int getQuantidade() {
return 0;
}
}
public class Main {
public static void main(String[] args) {
// Carrinho com itens
Carrinho carrinhoComItens = new CarrinhoUsuario(150.75, 3);
System.out.println("Valor: " + carrinhoComItens.getValor());
System.out.println("Quantidade: " + carrinhoComItens.getQuantidade());
// Carrinho nulo (não faz nada)
Carrinho carrinhoNulo = new CarrinhoNulo();
System.out.println("Valor: " + carrinhoNulo.getValor()); // Não precisa verificar null
System.out.println("Quantidade: " + carrinhoNulo.getQuantidade()); // Não precisa verificar null
}
}
Quando esse princípio é quebrado, pode ser mais interessante usar composição em vez de herança.
Por exemplo:
RelatorioPDF
eRelatorioExcel
podem ter comportamentos parecidos, mas forçar uma herança entre eles só porque "ambos são relatórios" pode gerar mais problemas do que soluções.
Uma abordagem melhor seria criar uma interface Processador
e compor os comportamentos em comum, conforme necessário.
Se for usar herança, tente se perguntar:
"Essa nova classe é uma variação da classe base, conceitualmente falando?"
Se a resposta for não, melhor repensar a abordagem (e considerar composição).
Algo interessante é o conceito de subtipo. Quando o LSP foi definido, ele era voltado apenas para classes, mas hoje o conceito de subtipo é mais amplo e pode ser entendido como:
Qualquer tipo que se encaixe no contrato esperado de outro tipo.