Princípios SOLID

Para começar, é importante deixar claro que SOLID não é um conjunto de regras rígidas e inflexíveis, e sim um guia de boas práticas para quem está construindo software. Esses princípios foram formulados para ajudar a escrever código que seja mais fácil de entender, manter e evoluir, mas isso não significa que eles devam ser aplicados “cegamente” em todas as situações. Em vez disso, pense no SOLID como recomendações úteis que você pode adaptar conforme o contexto e os desafios que estiver enfrentando no seu projeto.
A seguir, vamos fazer uma visão geral dos princípios e mostrar alguns exemplos ilustrativos para cada um deles.
O que é SOLID?
SOLID é um acrônimo que representa cinco princípios de design voltados para a Programação Orientada a Objetos (POO). Esses princípios foram penssados com o objetivo de incentivar a criação de código mais compreensível, testável e fácil de manter ao longo do tempo.
Em vez de focar apenas em “fazer funcionar”, o SOLID nos ajuda a estruturar melhor as classes e suas responsabilidades, tornando o sistema mais flexível e preparado para mudanças.
Cada letra do acrônimo SOLID representa um princípio específico da orientação a objetos:
- S – Single Responsibility Principle (SRP) – Princípio da Responsabilidade Única
- O – Open/Closed Principle (OCP) – Princípio Aberto/Fechado
- L – Liskov Substitution Principle (LSP) – Princípio da Substituição de Liskov
- I – Interface Segregation Principle (ISP) – Princípio da Segregação de Interfaces
- D – Dependency Inversion Principle (DIP) – Princípio da Inversão de Dependência
1 – Single Responsibility Principle (SRP) – Princípio da Responsabilidade Única
O Princípio da Responsabilidade Única (SRP) afirma que uma classe deve ter apenas uma única razão para mudar. Em outras palavras, ela deve ser responsável por uma única tarefa dentro do sistema.
Isso não significa que a classe precise ter apenas um método ou executar uma tarefa extremamente pequena, como às vezes é interpretado de forma equivocada. O foco do princípio está no motivo da mudança, e não na quantidade de métodos ou no tamanho da classe.
Quando misturamos responsabilidades diferentes na mesma classe, como regra de negócio, acesso a banco de dados e geração de relatórios, criamos um código mais difícil de manter e evoluir.
Por exemplo, se temos uma classe que representa um modelo de dados, como Usuario, Produto ou Fatura, ela deve mudar somente quando houver alteração na estrutura ou nas regras relacionadas a esses dados.
Da mesma forma, se uma classe é responsável pela persistência (como salvar ou buscar dados no banco), ela só deve ser modificada quando houver mudanças na lógica de acesso ou armazenamento.
Separar responsabilidades reduz o impacto das alterações e torna o código mais organizado e previsível.
❌ Exemplo que viola o Princípio da Responsabilidade Única
class Usuario
{
private string $nome;
private string $username;
private string $email;
private string $senha;
public function __construct(string $nome, string $username, string $email, string $senha)
{
$this->nome = $nome;
$this->username = $username;
$this->email = $email;
$this->senha = $senha;
}
public function salvar(): void
{
// persistir o usuario
}
public function enviarEmailBoasVindas(): void
{
// envio de email
}
}
Perceba que essa classe está assumindo três papéis diferentes: representar os dados do usuário, salvar essas informações no banco e ainda enviar e-mail.
Agora imagine que precisemos adicionar à classe os dados de conexão com o banco, como host, username e senha. Essas informações pertencem à infraestrutura da aplicação, não ao usuário do sistema e, além disso, são parecidas com os próprios dados do modelo. Ao alterar algo na conexão com o banco, estaríamos modificando a classe Usuario. Isso faz sentido? Será uma modificação óbvia de ser realizada? Claramente não.
A situação piora se também precisarmos armazenar dados de conexão com o servidor de e-mail. Teríamos mais atributos muito semelhantes, host, username e senha, mas com finalidades completamente diferentes. A classe começaria a acumular informações que não fazem parte do seu papel principal.
Surge então outro problema: como essas informações seriam recebidas? Pelo construtor, junto com os dados do usuário? Ou como parâmetros dos métodos? E mais: conseguiríamos testar apenas a persistência sem precisar configurar também o envio de e-mail?
Quando uma classe começa a fazer “coisas demais”, ela tende a se tornar confusa, difícil de testar e complicada de manter. É exatamente esse tipo de acoplamento desnecessário que o SRP busca evitar.
✅ Exemplo que segue o Princípio da Responsabilidade Única
class Usuario
{
private string $nome;
private string $username;
private string $email;
private string $senha;
public function __construct(string $nome, string $username, string $email, string $senha)
{
$this->nome = $nome;
$this->username = $username;
$this->email = $email;
$this->senha = $senha;
}
}
class UsuarioRepository
{
public function salvar(Usuario $usuario): void
{
// persistir o usuario
}
}
class EmailService{
public function enviarEmailBoasVindas(Usuario $usuario): void
{
// envio de email de boas vindas ao usuário
}
}
Agora temos uma classe para cada responsabilidade. Isso torna o código mais organizado, mais fácil de entender e permite que cada parte do sistema evolua de forma independente, sem gerar efeitos colaterais inesperados em outras áreas.
Cada funcionalidade pode ser modificada isoladamente, reduzindo o impacto das mudanças e melhorando a manutenção do sistema como um todo.
💡 Um ponto interessante: o padrão Active Record, presente em alguns frameworks, costuma concentrar regras de negócio e persistência na mesma classe. Essa abordagem pode violar o SRP, tornando o código mais acoplado e, em muitos casos, mais difícil de testar.
2 – Open/Closed Principle (OCP) – Princípio Aberto/Fechado
O Princípio Aberto/Fechado (OCP) afirma que entidades de software, como classes, módulos e funções, devem estar abertas para extensão, mas fechadas para modificação.
Na prática, isso significa que devemos conseguir adicionar novos comportamentos ao sistema sem alterar código que já está funcionando e testado. Em vez de modificar uma classe existente sempre que surge uma nova regra, devemos estendê-la por meio de abstrações.
Normalmente, essa extensão é realizada utilizando interfaces e classes abstratas, permitindo que novos comportamentos sejam adicionados por meio de novas implementações, sem alterar a estrutura original.
❌ Exemplo que viola o Princípio Aberto/Fechado
class ProcessadorDePagamento
{
public function processarPagamento(Cliente $cliente, float $valor, string $metodo):void
{
// algum código como log, gravar em banco, etc
if($metodo === "Cartão"){
// processa pagamento via cartão
}else if($metodo === "Boleto"){
// processa pagamento via boleto
}else if($metodo === "Pix"){
// processa pagamento via boleto
}
}
}
Esse é um exemplo clássico de violação do Princípio Aberto/Fechado. Sempre que surge um novo método de pagamento, somos obrigados a modificar a classe, adicionando mais um if ou else if.
O problema é que, ao alterar código que já está funcionando, corremos o risco de impactar comportamentos que já estavam implementados e testados (ou pelo menos deveriam estar). Quanto mais modificações acumulamos nesse mesmo ponto, maior a chance de introduzir erros ou regressões.
✅ Exemplo que segue o Princípio Aberto/Fechado
interface MetodoPagamento
{
function processar(Cliente $cliente, float $valor);
}
class Cartao implements MetodoPagamento
{
function processar(Cliente $cliente, float $valor): void
{
// processa pagamento via cartão
}
}
class Boleto implements MetodoPagamento
{
function processar(Cliente $cliente, float $valor): void
{
// processa pagamento via boleto
}
}
class Pix implements MetodoPagamento
{
function processar(Cliente $cliente, float $valor): void
{
// processa pagamento via pix
}
}
class ProcessadorDePagamento
{
public function processarPagamento(Cliente $cliente, float $valor, MetodoPagamento $metodo):void
{
// algum código como log, gravar em banco, etc
$metodo->processar($cliente, $valor);
}
}
Agora o ProcessadorDePagamento não precisa mais saber qual é o método de pagamento utilizado, já que ele recebe o objeto responsável por parâmetro. Ele depende apenas da abstração MetodoPagamento.
Se precisarmos adicionar um novo meio de pagamento, como Transferencia, basta criar uma nova classe que implemente a interface. Nenhuma alteração é necessária no ProcessadorDePagamento.
Dessa forma, o sistema está aberto para extensão (podemos criar novos métodos) e fechado para modificação (não alteramos código já estável).
3 – Liskov Substitution Principle (LSP) – Princípio da Substituição de Liskov
O Princípio da Substituição de Liskov (LSP) afirma que subclasses devem poder substituir suas classes base sem alterar o comportamento esperado do sistema.
Em termos práticos, se temos uma classe A e uma classe B que estende A, deve ser possível passar um objeto de B para qualquer método que espere um objeto do tipo A, sem que isso gere comportamentos inesperados ou quebre a lógica da aplicação. Esse é exatamente o comportamento esperado quando utilizamos herança e polimorfismo.
A ideia central é que a classe filha deve especializar o comportamento da classe pai, e não contradizê-lo ou reduzi-lo.
Violamos esse princípio quando criamos uma classe filha que não consegue cumprir corretamente o contrato definido pela classe base. Um exemplo comum é quando sobrescrevemos um método e o deixamos vazio, retornamos valores inconsistentes (como null ou 0 sem sentido semântico) ou lançamos exceções apenas porque aquele comportamento não faz sentido para a subclasse.
Quando isso acontece, significa que a herança foi usada de forma inadequada.
❌ Exemplo que viola o Princípio da Substituição de Liskov
class Retangulo
{
protected int $largura;
protected int $altura;
public function __construct(int $largura, int $altura)
{
$this->largura = $largura;
$this->altura = $altura;
}
public function setLargura(int $largura): void
{
$this->largura = $largura;
}
public function setAltura(int $altura): void
{
$this->altura = $altura;
}
public function getLargura(): int
{
return $this->largura;
}
public function getAltura(): int
{
return $this->altura;
}
public function getArea(): int
{
return $this->largura * $this->altura;
}
}
class Quadrado extends Retangulo
{
public function __construct(int $lado)
{
$this->largura = $lado;
$this->altura = $lado;
}
public function setLargura(int $largura): void
{
$this->largura =$largura;
$this->altura = $largura;
}
public function setAltura(int $altura): void
{
$this->altura = $altura;
$this->largura = $altura;
}
}
Nesse exemplo, o código viola o Princípio da Substituição de Liskov. Embora, do ponto de vista matemático, um quadrado seja um tipo de retângulo, na modelagem orientada a objetos a herança precisa preservar o comportamento definido pela classe base.
A classe Retangulo estabelece, implicitamente, um contrato: largura e altura podem variar de forma independente. Já a classe Quadrado quebra essa expectativa ao forçar que qualquer alteração na largura também modifique a altura, e vice-versa.
O problema fica evidente quando um objeto Quadrado é passado para um método que espera um Retangulo e altera apenas a largura. Quem escreveu esse método espera que apenas a largura seja modificada. No entanto, no caso do Quadrado, a altura também será alterada automaticamente.
Isso significa que a substituição mudou o comportamento esperado, e é exatamente esse tipo de inconsistência que o LSP busca evitar.
Taçvez com um exemplo talvez fique mais claro:
function aumentarLarguraRetangulo(Retangulo $retangulo){
$retangulo->setLargura( $retangulo->getLargura() * 2);
}
$retangulos = [
new Retangulo(10, 20),
new Quadrado(15)
];
foreach($retangulos as $r){
aumentarLarguraRetangulo($r);
echo "Largura: " . $r->getLargura(). PHP_EOL;
echo "Altura: " . $r->getAltura(). PHP_EOL;
echo "--------" . PHP_EOL;
}
Perceba que o método aumentarLarguraRetangulo foi criado com a intenção clara de modificar apenas a largura. No entanto, ao iterarmos sobre um array de Retangulo que contém um objeto Quadrado “disfarçado” ali no meio, a chamada ao método acaba alterando também a altura do quadrado.
Essa modificação indireta não é perceptível para quem escreveu a função e foge completamente da expectativa original. Ou seja, temos um comportamento inesperado, que pode gerar efeitos colaterais difíceis de identificar e, consequentemente, introduzir bugs no sistema.
✅ Exemplo que segue o Princípio Princípio da substituição de Liskov
interface Forma
{
public function getArea():int;
}
class Retangulo implements Forma
{
private int $largura;
private int $altura;
public function __construct(int $largura, int $altura)
{
$this->largura = $largura;
$this->altura = $altura;
}
public function setLargura(int $largura): void
{
$this->largura = $largura;
}
public function setAltura(int $altura): void
{
$this->altura = $altura;
}
public function getLargura(): int
{
return $this->largura;
}
public function getAltura(): int
{
return $this->altura;
}
public function getArea(): int
{
return $this->largura * $this->altura;
}
}
class Quadrado implements Forma
{
private int $lado;
public function __construct(int $lado)
{
$this->lado = $lado;
}
public function setLado(int $lado): void
{
$this->lado = $lado;
}
public function getArea(): int
{
return $this->lado * $this->lado;
}
}
Neste caso, fazer Quadrado herdar de Retangulo não é a melhor escolha de modelagem. Uma abordagem mais adequada é extrair uma abstração comum, como a interface Forma, que represente apenas o comportamento essencial compartilhado por ambos. Assim, Quadrado e Retangulo passam a implementar o mesmo contrato, compartilhando apenas aquilo que realmente têm em comum, sem impor regras comportamentais incompatíveis.
4 – Interface Segregation Principle (ISP) – Princípio da Segregação de Interfaces
O Princípio da Segregação de Interfaces estabelece que nenhuma classe deve ser obrigada a implementar métodos que não utiliza. Em vez de criar uma única interface ampla e genérica, é preferível definir várias interfaces menores e mais específicas. Dessa forma, evitamos forçar classes a depender de métodos que não fazem sentido para sua responsabilidade, reduzindo acoplamento e aumentando a coesão do sistema.
❌ Exemplo que viola o Princípio da Segregação de Interfaces
interface Pagamento
{
function pagar(float $valor): void;
function processarParcelamento(int $parcelas): void;
function gerarComprovanteFiscal(): void;
function validarCodigoSeguranca(string $codigo): void;
}
class CartaoCredito implements Pagamento
{
public function pagar(float $valor): void
{
// realizar pagamento
}
public function processarParcelamento(int $parcelas): void
{
// processar parcelamento
}
public function gerarComprovanteFiscal(): void
{
// gerar comprovante fiscal
}
public function validarCodigoSeguranca(string $codigo): void
{
// validar código de segurança
}
}
class Pix implements Pagamento
{
public function pagar(float $valor): void
{
// realizar pagamento
}
public function processarParcelamento(int $parcelas): void
{
throw new Exception("Não implementado");
}
public function gerarComprovanteFiscal(): void
{
// gerar comprovante fiscal
}
public function validarCodigoSeguranca(string $codigo): void
{
throw new Exception("Não implementado");
}
}
Observe que, nesse cenário, a classe Pix foi obrigada a implementar os métodos processarParcelamento() e validarCodigoSeguranca(), mesmo que essas operações não façam sentido para esse meio de pagamento.
Para contornar isso, a implementação lançou exceções, deixando explícito que o comportamento não é suportado. No entanto, essa solução apenas mascara um problema de design: a interface foi definida com responsabilidades que nem todas as classes conseguem cumprir. Aqui o lançamento de exceção não é o erro, mas apenas um sintoma, o problema está na interface Pagamento que concentra muitas responsabilidades.
✅ Exemplo que segue o Princípio da Segregação de Interfaces
interface Pagamento
{
function pagar(float $valor) : void;
function gerarComprovanteFiscal(): void;
}
interface Parcelavel
{
function processarParcelamento(int $parcelas): void;
}
interface Validavel
{
function validarCodigoSeguranca(string $codigo): void;
}
class CartaoCredito implements Pagamento, Parcelavel, Validavel
{
public function pagar(float $valor): void
{
// realizar pagamento
}
public function processarParcelamento(int $parcelas): void
{
// processar parcelamento
}
public function gerarComprovanteFiscal(): void
{
// gerar comprovante fiscal
}
public function validarCodigoSeguranca(string $codigo): void
{
// validar código de segurança
}
}
class Pix implements Pagamento
{
public function pagar(float $valor): void
{
// realizar pagamento
}
public function gerarComprovanteFiscal(): void
{
// gerar comprovante fiscal
}
}
Agora a classe Pix implementa apenas as interfaces que realmente utiliza. Ela depende exclusivamente dos comportamentos que fazem sentido para esse meio de pagamento, mantendo o código mais coeso e menos acoplado.
Se, no futuro, identificarmos que algum tipo de pagamento não precisa gerar comprovante fiscal, poderemos extrair o método gerarComprovanteFiscal() para uma interface específica, como ComprovanteFiscal. Dessa forma, mantemos os contratos enxutos e alinhados com as responsabilidades reais de cada classe.
5 – Dependency Inversion Principle (DIP) – Princípio da Inversão da Dependência
O Princípio da Inversão da Dependência estabelece que não devemos depender de implementações concretas, mas sim de abstrações. Em outras palavras, nossas classes devem depender de interfaces ou classes abstratas, e não de classes concretas diretamente.
Isso significa que a regra de negócio não deve conhecer detalhes de implementação. Em vez de instanciar objetos específicos, ela deve trabalhar com contratos bem definidos, deixando que os detalhes concretos sejam fornecidos externamente.
❌ Exemplo que viola o Princípio da Inversão da Dependência
class MySQLDatabase
{
public function connect()
{
// conectar com o banco mysql
}
}
class UsuarioService
{
private MySQLDatabase $database;
public function __construct()
{
$this->database = new MySQLDatabase();
}
public function save(Usuario $usuario)
{
$con = $this->database->connect();
// salvar usuario
}
}
Nesse cenário, se quisermos trocar o banco de dados de MySQL para PostgreSQL, não basta apenas criar uma nova classe de conexão. Também será necessário modificar a própria classe UsuarioService, alterando o ponto onde o objeto é instanciado.
Ou seja, uma mudança de infraestrutura obriga a alteração na regra de negócio. Isso aumenta o acoplamento e torna o código mais rígido, exatamente o tipo de problema que o Princípio da Inversão da Dependência busca evitar.
✅ Exemplo que segue o Princípio da Inversão da Dependência
interface Database
{
public function connect();
}
class MySQLDatabase implements Database
{
public function connect()
{
// conectar com o banco mysql
}
}
class PostgresDatabase implements Database
{
public function connect()
{
// conectar com o banco postgres
}
}
class UsuarioService
{
private Database $database;
public function __construct(Database $database)
{
$this->database = $database;
}
public function save(Usuario $usuario)
{
$con = $this->database->connect();
// salvar usuario
}
}
Agora podemos trocar o banco de dados utilizado pela UsuarioService simplesmente passando uma implementação diferente no construtor.
Como o serviço depende da abstração Database, e não de uma classe concreta, qualquer classe que implemente essa interface pode ser utilizada sem que seja necessário alterar a regra de negócio.
Na prática, isso significa mais flexibilidade, menor acoplamento e um código muito mais preparado para mudanças.
Conclusão
Ao longo do artigo, vimos os princípios que compõem o SOLID. Se você observar com atenção, perceberá que eles se complementam: muitas vezes, ao aplicar corretamente um deles, outros acabam sendo atendidos naturalmente.
Também é importante lembrar que esses princípios não são regras absolutas e imutáveis. Eles servem como diretrizes para nos ajudar a escrever código mais limpo, extensível e testável, não como algo “escrito em pedra”, mas como boas práticas que orientam nossas decisões de design no dia a dia.
Com o passar do tempo, esses princípios acabam se tornando naturais. Muitas vezes, quando não os seguimos, o código simplesmente parece “errado”, mesmo que ainda funcione.
Bom era isso. T++
