Princípios SOLID e outras sopas de letrinhas
Todo mundo(ou quase todo) já ouviu esse acrônimo em algum lugar, se estamos em ínicio de carreira então... Somos bombardeados de nomezinhos mágicos que dizem que temos que saber e usar, do meu lugar eu diria que clean code e SOLID foram os que eu mais vi e ouvi serem propagados. Mas o que é SOLID? Cada letra desse acrônimo é relacionada a um principio e cada principio se relaciona com algum conceito importante de orientação objetos. O primeiro, SRP, é o single responsability, principio da responsabilidade única. Geralmente o principio mais simples de entender e absorver(ao menos do meu ponto de vista) e que em algum nível todos praticamos no dia a dia. O principio é intuitivo, o seu método, a sua classe, devem ter somente um motivo para mudar, ou seja somente uma responsabilidade. Que é justamente o significado de um termo bem importante em OOP, coesão. Em alguns casos é bem fácil identificar que uma classe não é coesa: class User { public function __construct( private string $name, private string $email ){}; public function saveToDatabase() { echo "Salvando usuário no banco de dados...\n"; } public function sendWelcomeEmail() { echo "Enviando e-mail de boas-vindas para {$this->email}...\n"; } } O exemplo acima trás uma classe de usuário que de fato representa um usuário com seu email e senha, mas também faz outras coisas que não deveria fazer, persistir os dados e enviar emails. Neste caso poderiamos ter a classe de usuários que passaria a ter somente a responsabilidade de lidar com os dados do usuário, cria-se a classe UserRepository que fica responsável pelas operações no banco de dados e um novo service que fica responsável por disparo de emails. class User { public function __construct( private string $name, private string $email ) {}; public function getName(): string { return $this->name; } public function getEmail(): string { return $this->email; } } class UserRepository { public function save(User $user): void { echo "Salvando usuário {$user->getName()} no banco de dados...\n"; } } class EmailService { public function sendWelcomeEmail(User $user): void { echo "Enviando e-mail de boas-vindas para {$user->getEmail()}...\n"; } } Mas nem sempre só porque um classe tem um único método ou aparenta ter poucas responsabilidades ela é de fato coesa. O service de email, por exemplo, até pode ter somente a função de enviar emails, mas ela pode acabar não tendo um único motivo para mudar: class EmailService { public function sendWelcomeEmail(User $user): void { echo "Enviando e-mail de boas-vindas para {$user->getEmail()}...\n"; } public function sendPasswordResetEmail(User $user): void { echo "Enviando e-mail de recuperação de senha para {$user->getEmail()}...\n"; } public function sendPromotionEmail(User user, string user,stringpromotion): void { echo "Enviando promoção 'promotion' para {promotion' para {user->getEmail()}...\n"; } public function sendAccountDeactivationEmail(User $user): void { echo "Enviando e-mail de desativação de conta para {$user->getEmail()}...\n"; } } Se a lógica do e-mail de promoção mudar, a classe muda. Se for necessário um template para cada tipo de email, a classe muda. Se somente um email em especifico precisar ter algum anexo, a classe muda. Poderíamos resolver esse problema então, tendo uma interface que faz envio de email, cada tipo de email tem sua própria classe e a lógica específica de cada email não faz uma única classe crescer infinitamente. Além de ficar fácil testar, manter e estender comportamentos. interface Email { public function send(): void; } class WelcomeEmail implements Email { public function __construct(private User $user) {}; public function send(): void { echo "Enviando e-mail de boas-vindas para {$this->user->getEmail()}...\n"; } } class PasswordResetEmail implements Email { public function __construct(private User $user) {}; public function send(): void { echo "Enviando e-mail de recuperação de senha para {$this->user->getEmail()}...\n"; } } class PromotionEmail implements Email { public function __construct(private User user, private string user,privatestringpromotion) {}; public function send(): void { echo "Enviando promoção '{this->promotion}' para {this->promotion}' para {this->user->getEmail()}...\n"; } } Poderíamos ter uma classe que faz o envio de emails, e é ela a quem podemos chamar, passando apenas qual é o email que queremos enviar: `class EmailSender { public function send(Email $email): void { $email->send(); } } $user = new User("Maria", "maria@email.com"); $emailSender = new EmailSender(); emailSender->send(new WelcomeEmail($user));` E como identificar que uma classe faz mais do que deve

Todo mundo(ou quase todo) já ouviu esse acrônimo em algum lugar, se estamos em ínicio de carreira então... Somos bombardeados de nomezinhos mágicos que dizem que temos que saber e usar, do meu lugar eu diria que clean code e SOLID foram os que eu mais vi e ouvi serem propagados.
Mas o que é SOLID?
Cada letra desse acrônimo é relacionada a um principio e cada principio se relaciona com algum conceito importante de orientação objetos.
O primeiro, SRP, é o single responsability, principio da responsabilidade única. Geralmente o principio mais simples de entender e absorver(ao menos do meu ponto de vista) e que em algum nível todos praticamos no dia a dia.
O principio é intuitivo, o seu método, a sua classe, devem ter somente um motivo para mudar, ou seja somente uma responsabilidade. Que é justamente o significado de um termo bem importante em OOP, coesão.
Em alguns casos é bem fácil identificar que uma classe não é coesa:
class User {
public function __construct(
private string $name,
private string $email
){};
public function saveToDatabase() {
echo "Salvando usuário no banco de dados...\n";
}
public function sendWelcomeEmail() {
echo "Enviando e-mail de boas-vindas para {$this->email}...\n";
}
}
O exemplo acima trás uma classe de usuário que de fato representa um usuário com seu email e senha, mas também faz outras coisas que não deveria fazer, persistir os dados e enviar emails.
Neste caso poderiamos ter a classe de usuários que passaria a ter somente a responsabilidade de lidar com os dados do usuário, cria-se a classe UserRepository que fica responsável pelas operações no banco de dados
e um novo service que fica responsável por disparo de emails.
class User {
public function __construct(
private string $name,
private string $email
) {};
public function getName(): string {
return $this->name;
}
public function getEmail(): string {
return $this->email;
}
}
class UserRepository {
public function save(User $user): void {
echo "Salvando usuário {$user->getName()} no banco de dados...\n";
}
}
class EmailService {
public function sendWelcomeEmail(User $user): void {
echo "Enviando e-mail de boas-vindas para {$user->getEmail()}...\n";
}
}
Mas nem sempre só porque um classe tem um único método ou aparenta ter poucas responsabilidades ela é de fato coesa. O service de email, por exemplo, até pode ter somente a função de enviar emails, mas ela pode acabar não tendo um único motivo para mudar:
class EmailService {
public function sendWelcomeEmail(User $user): void {
echo "Enviando e-mail de boas-vindas para {$user->getEmail()}...\n";
}
public function sendPasswordResetEmail(User $user): void {
echo "Enviando e-mail de recuperação de senha para {$user->getEmail()}...\n";
}
public function sendPromotionEmail(User user, string user,stringpromotion): void {
echo "Enviando promoção 'promotion' para {promotion' para {user->getEmail()}...\n";
}
public function sendAccountDeactivationEmail(User $user): void {
echo "Enviando e-mail de desativação de conta para {$user->getEmail()}...\n";
}
}
Se a lógica do e-mail de promoção mudar, a classe muda. Se for necessário um template para cada tipo de email, a classe muda. Se somente um email em especifico precisar ter algum anexo, a classe muda.
Poderíamos resolver esse problema então, tendo uma interface que faz envio de email, cada tipo de email tem sua própria classe e a lógica específica de cada email não faz uma única classe crescer infinitamente. Além de ficar fácil testar, manter e estender comportamentos.
interface Email {
public function send(): void;
}
class WelcomeEmail implements Email {
public function __construct(private User $user) {};
public function send(): void {
echo "Enviando e-mail de boas-vindas para {$this->user->getEmail()}...\n";
}
}
class PasswordResetEmail implements Email {
public function __construct(private User $user) {};
public function send(): void {
echo "Enviando e-mail de recuperação de senha para {$this->user->getEmail()}...\n";
}
}
class PromotionEmail implements Email {
public function __construct(private User user, private string user,privatestringpromotion) {};
public function send(): void {
echo "Enviando promoção '{this->promotion}' para {this->promotion}' para {this->user->getEmail()}...\n";
}
}
Poderíamos ter uma classe que faz o envio de emails, e é ela a quem podemos chamar, passando apenas qual é o email que queremos enviar:
`class EmailSender {
public function send(Email $email): void {
$email->send();
}
}
$user = new User("Maria", "maria@email.com");
$emailSender = new EmailSender();
emailSender->send(new WelcomeEmail($user));`
E como identificar que uma classe faz mais do que deveria? Além de identificar classes com métodos com responsabilidades totalmente distintas, também podemos observar classes com métodos que podem crescer descontroladamente, classes com comportamentos que variam muito com a entrada e essa entrada pode mudar também (cálculo de salário com base no cargo? Muitas condicionais pra fazer essa avaliação? Aumentou um cargo, aumenta uma condicional).
Tem mudado frequentemente a classe? Tudo isso pode ser sinal de que a classe poderia ser mais coesa e ter menos responsabilidades.
Ainda tratando do email e chegando a letra O, temos o Open Closed Principle (principio do aberto-fechado - OCP), a ideia é que as classes sejam abertas para a extensão e fechadas para modificação. Isso quer dizer que devemos poder sempre criar novas coisas, mas sem que seja necessário alterar o código o tempo todo.
Se o exemplo anterior usando para o email ao invés de possuir uma classe send simples possuisse um método com inúmeros if/else a cada novo tipo de email precisariamos criar um novo elseif:
class EmailSender {
public function send(string $type, User $user, string $extra = '') {
if ($type === 'welcome') {
echo "Enviando e-mail de boas-vindas para {$user->getEmail()}...\n";
} elseif ($type === 'password_reset') {
echo "Enviando e-mail de recuperação de senha para {$user->getEmail()}...\n";
} elseif ($type === 'promotion') {
echo "Enviando promoção '$extra' para {$user->getEmail()}...\n";
}
}
}
O que vai contra o OCP, justamente porque não queremos mexer na classe a todo momento.
Se quisermos criar um novo email para desejar feliz aniversário aos clientes com a nossa estrutura original o que fazemos? Criamos uma nova classe de email:
class BirthdayEmail implements Email {
public function __construct(private User $user) {};
public function send(): void {
echo "Feliz Aniversário {$this->user->getName()}...\n";
}
}
E basta chamar o sender passando o novo email para utilizá-lo
$user = new User("Joao", "joao@email.com");
$emailSender = new EmailSender();
emailSender->send(new BirthdayEmail($user));`
Assim, criamos uma nova estrutura sem mudar a atual. Respeitamos o OCP. Mas porque esse tipo de ação é importante? Para evitar acoplamentos. Sempre devemos pensar em abstrações durante a resolução dos problemas, elas nos ajudarão a ter clareza do problema e tornar nosso código fácil de ser extendido.
Na programação orientada a objetoos pensamos e lidamos o tempo todo com acoplamento e coesão e as abstrações são parte fundamental para construirmos sistemas que evolui bem, quando chegarmos a letra D entraremos mais detalhadamente no assunto do acoplamento.
Antes, falemos do principio de Liscov (LSP), nossa letra L, o principio que leva o nome de sua criadora, Bárbara Liskov(ouça um pouco direto da fonte) foi definido em 1988 e num primeiro momento tem uma definição que não aparenta ser muito amigável:
"Se para cada objeto o1 do tipo S houver um objeto o2 do tipo T tal que para todos os programas P definidos em termos de T, o comportamento de P permanece inalterado quando o1 é substituído por o2, então S é um subtipo de T." (versão traduzida). Basicamente o que todo esse enunciado quer dizer é que para uma classe herdar de outra devemos respeitar seu contrato, se o pai não lança algum tipo de exceção, o filho também não deveria, por exemplo.
O livro da casa do código "Orientação a Objetos e SOLID para Ninjas" apresenta uma explicação que deixa as coisas ainda mais claras, dizendo que nunca devemos apertar uma pré-condição(entrada de dados) e nunca afrouxar uma pós-condição(saída), se minha classe pai recebe um inteiro entre 0 e 100 como parâmetro, meu filho até pode receber um inteiro entre 0 e 200, mas uma classe pai que retorna somente de 0 a 100, não pode ter um filho que retorna de 0 a 200, as estruturas que se aproveitam desse código podem não suportar esse range maior, mas se o filho quiser passar a saída de 100 para 50 isso não seria um problema, já que quem lida com 0 a 100 lidaria tranquilamente com 0 a 50.
Aproveitando a minha estrutura de usuário:
class User {
public function __construct(
private string $name,
private string $email
) {} public function getName(): string {
return $this->name;
} public function getEmail(): string {
return $this->email;
}
}
se eu passo a ter uma classe funcionário que herda de usuário desta maneira:
class Employee extends User {
public function getEmail(): string {
return "contato@empresa.com";
}
Eu quebro o principio de Liskov, isso porque quem usa uma classe que herda de usuário espera que ao buscar o email de um usuário estanciado por essa classe receba o email real do usuário e não um email genérico.
se minha classe fosse
class Employee extends User {
public function getEmail(): string {
throw new Exception("Funcionários sem email.");
}
}
teríamos o mesmo problema, quem espera um email de usuário não espera lidar com nenhum tipo de exceção.
Pulando para a próxima letra do nosso acrononimo, I, temos o principio ISP(Interface Segregation Principle - principio de segregação de interfaces), que nos leva a pensar em como devemos construir nossa interface. Assim como classes com implementação, interfaces também devem ser coesas. Interfaces coesas tem comportamento simples e bem definido. Devemos evitar que as interfaces tenham métodos que sejam desnecessários para quem a implementa.
Novamente pensando na interface de email a seguir
interface Email {
public function send(): void;
}
se eu resolver incluir a possibilidade de enviar emails via agendamento de email minha interface ficaria assim
interface Email {
public function send(): void;
public function schedule(DateTime $time): void;
}
e neste momento ela violaria o ISP, isso porque nem todo tipo de email precisaria de um agendamento, levando a classe a ser usada desperdiçando implementação, digamos assim. Mas como seria o ideal?
Termos dois tipos de interface, uma que faz o envio padrão, a outra que faz o agendamento.
interface Emailable {
public function send(): void;
}
interface SchedulableEmail {
public function schedule(DateTime $time): void;
}
o nosso antigo email de boas vindas agora pode implementar Emailable
class WelcomeEmail implements Emailable {
public function __construct(private User $user) {}
public function send(): void {
echo "Enviando e-mail de boas-vindas para {$this->user->getEmail()}...\n";
}
}
mas nosso email promocional, ao invés de um email imediato pode querer um agendamento, utilizando SchedulableEmail
class PromotionEmail implements SchedulableEmail {
public function __construct(private User user, private string user,privatestringpromotion) {}
public function schedule(DateTime $time): void {
echo "Agendado: '{this->promotion}' será enviado para {this->promotion}' será enviado para {this->user->getEmail()} em {$time->format('Y-m-d H:i:s')}.\n";
}
}
um outro email inclusive pode precisar ter as duas funções e precisar das duas interfaces, consegue pensar em algum cenário?
Chegando ao nossa última letrinha, temos o principio de inversão de dependências, o DIP(dependency inversion principle), para falarmos deste principio precisamos entrar um pouco mais fundo no acoplamento, você alguma vez já ouviu que nossa classe deve ser acoplada a outras o mínimo possível? Eu já, milhares de vezes. Mas porque? O principal motivo é que quando uma classe é muito dependente de outra qualquer mudança por menor e mais inofensiva que pareça pode propagar problemas por todas as classes dependentes delas, além disso, o reuso das classes também se complica, vamos sempre carregar a classe e todas as dependências que ela tiver.
Hora de fazer uma guerra contra o acoplamento e não permitir nenhum, que tal? Mais fácil acabar com o mal pela raiz, né. Na verdade não, nem todo acoplamento é nocivo, o próprio uso de interfaces do principio anterior é um exemplo de uso de acoplamento, mas que se bem feito não é danoso a nossa aplicação. E como seria um acoplamento bem feito? Para um acoplamento ser bem feito devemos acoplar nossas classes somente em classes estáveis, classes estáveis são aquelas que nunca mudam ou que mudam muito pouco. Elas vão ter menos chance de propagar problemas.
Então se precisamos de classes estáveis e interfaces geralmente são estáveis, que tal usar interface pra tudo?
Também não é a solução, aqui chegamos no famoso 'depende'. O que é melhor? Depende. Ok que você não quer acoplamento, mas será que de fato precisa da flexibilidade que as interfaces fornecem a todo instante? Será que de fato ela é uma interface estável? Já não está acoplando em coisas demais? Tudo vai da análise durante o desenvolvimento.
Uma maneira interessante de pensar em DIP é pensar que abstrações não devem depender de detalhes(e/ou implementação), detalhes devem depender de abstrações. Detalhes mudam mais do que abstrações mudam. E isso nos leva a um problema de DIP no meu exemplo de email promocional.
class PromotionEmail implements SchedulableEmail {
public function __construct(private User user, private string user,privatestringpromotion) {}
public function schedule(DateTime $time): void {
echo "Agendado: '{this->promotion}' será enviado para {this->promotion}' será enviado para {this->user->getEmail()} em {$time->format('Y-m-d H:i:s')}.\n";
}
}
A classe que envia é dependente da forma como eu agendo o email (usando o echo), não temos uma abstração para o agendamento. Nesse caso o primeiro passo para correção é a criação de uma abstração, algo como:
interface EmailScheduler {
public function schedule(SchedulableEmail $email, DateTime $time): void;
}
A partir dai eu posso ter uma implementação que use echo, mas que no futuro eu posso trocar facilmente por qualquer outra coisa sem maiores problemas:
class ConsoleEmailScheduler implements EmailScheduler {
public function schedule(SchedulableEmail $email, DateTime $time): void {
echo "[Agendador] ";
$email->schedule($time);
}
}
Então posso criar uma nova interface para qualquer email que queira ser agendado:
interface SchedulableEmail {
public function schedule(DateTime $time): void;
}
e seguirmos com a classe de promoção assim:
class PromotionEmail implements SchedulableEmail {
public function __construct(private User user, private string user,privatestringpromotion) {}
public function schedule(DateTime $time): void {
echo "Email promocional '{this->promotion}' será enviado para {this->promotion}' será enviado para {this->user->getEmail()} em {$time->format('Y-m-d H:i:s')}.\n";
}
}
A diferença fica no uso das estruturas, que na estrutura original precisa ser
$email = new PromotionEmail($user, "Promoção");
$email->schedule(new DateTime());
Então a classe que instancia o PromotionEmail também está decidindo como agendar e o schedule() faz um echo direto, acoplando o agendamento a classe. Com a nova estrutura temos que
$email = new PromotionEmail($user, "Promoção");
$scheduler = new ConsoleEmailScheduler();
$scheduler->schedule($email, new DateTime());
Agora a classe PromotionEmail não toma a decisão de como agendar. Quem chama schedule() agora é o agendador externo (ConsoleEmailScheduler), que pode mudar.
DRY
Vamos aproveitar e falar de outras sopas?
DRY quer dizer Don't Repeat Yourself, é um príncipio que nos diz que não devemos repetir código, devemos poder reusar o nosso código o máximo possível. Ele conversa completamente com o SRP, isso porque evitando duplicação de código a tendência é criarmos métodos e classes mais coesos, facilitando o cumprimento do SRP.
KISS
Este principio diz Keep It Simple, Stupid. A ideia é manter o código o mais simples possível, ter métodos pequenos, objetivos, principalmente autoexplicativo. Sempre evitando dependências e estruturas desnecessárias. Isso ajuda a manter um código testável e escalável.
Se você usa classes herança e interfaces demais tentando cumprir OCP talvez você viole KISS. No DIP, se pensarmos no benefício de flexibilidade trazido pela abstração podemos ter sucesso, mas se exagerarmos em abstrações podemos violar o KISS. Nesse ponto o ideal é usar as abstrações quando realmente tivermos necessidade de desacoplamento.
Equílibrio
Diante de tantos principios, de tantas regras, tantas decisões, devemos não tentar ser sempre 8 ou 80, sempre pensar no equilíbrio que o nosso sistema precisa, entender qual a sua maior necessidade e ai sim aplicar aquilo que faça mais sentido para o nosso contexto.
Referências:
- Livro "Orientação a Objetos e SOLID para ninjas" - Casa do Código
- Publicação O que é SOLID: O guia completo para você entender os 5 principios da POO" - João Roberto da Paixão
- Vídeo : SOLID Principles: Do You Really Understand Them?
- Publicação Conheça os princípios DRY, KISS e YAGNI
- Publicação How do you simplify your code with DRY and KISS principles?