Esses principios foram concebidos pela primeira vez por Robert C. Martin em seu artigo do ano 2000, Design Principles and Design Patterns. Esses conceitos foram construidos posteriormente por Michael Feathers, quem nos introduziu o acrônimo SOLID.
Seguindo esses 5 conceitos temos:
Vamos explicar a fundo e na prática usando diversos exemplos sobre cada um deles e o que eles significam.
Começando com o primeiro princípio do acrônimo, a responsabilidade única afirma que uma classe deveria somente ter uma única responsabilidade.
E como isso poderia nos ajudar na construção de um software melhor ?
-
Uma classe com uma única responsabilidade terá menos casos de testes.
-
Menos funcionalidade em uma única classe terá menos dependência.
-
Melhora na organização, classes menores e bem organizadas ficam mais fáceis de encontrar em um projeto.
Vamos ter uma melhor ideia disso na prática.
class Pessoa
{
protected $nome;
protected $idade;
protected $altura;
public function __construct($nome, $idade, $altura)
{
$this->nome = $nome;
$this->idade = $idade;
$this->altura = $altura;
}
public function verificaMaioridade()
{
if ($this->idade < 18) {
echo "{$this->nome} é menor de idade";
} else {
echo "{$this->nome} é maior de idade";
}
}
/*
* Ações
*
*/
public function andar()
{
/* ... */
}
public function correr()
{
/* ... */
}
}
Esse código funciona sem nenhum erro, porém, viola o princípio. Note que métodos foram adicionados que não necessariamente 'combinam' com a classe. Poderiamos resolver ou melhorar isso criando uma nova classe PessoaAction para definir todas as ações que a pessoa pode ter.
class PessoaAction extends Pessoa
{
public function andar()
{
/* ... */
}
public function correr()
{
/* ... */
}
public function pular()
{
/* ... */
}
}
Podemos aproveitar essa nova classe criada adicionar todos os métodos (ações no caso) que a classe mãe pode requirir, assim na medida que o projeto crescer, será muito mais fácil identificar o que cada arquivo de classe deve fazer.
Simplificando esse princípio, classes deveriam ser abertas para extensão, mas fechadas para modificação. Ao fazer isso, paramos de modificar o código existente e causar novos bugs potenciais. Claro, a única exceção à regra é ao consertar bugs no código existente.
Exemplificando:
Imagine que como parte de um novo projeto, nós implementamos uma classe Carro.
Ela é bem complexa e com vários atributos e métodos.
class Carro
{
private $marca;
private $modelo;
private $vel;
// Construtores, getters e setters ...
}
Nós lançamos o projeto e todo mundo aprovou. Entretando, depois de poucos meses, nós decidimos que a classe Carro está um pouco 'vazia' e poderiamos adicionar atributos de carros que tenham funcionalidades únicas.
Nesse caso, abrimos a classe Carro e adicionamos esses atributos, porém, quem sabe quais erros isso pode ocasionar em seu projeto ?
Ao invés disso, vamos usar o princípio Open/Closed e simplesmente extender a classe Carro.
class CarroRaro extends Carro
{
private $atributoUnicoDoCarroRaro;
// Construtores, setters e getters ...
}
Por extender nossa classe Carro podemos ter certeza que nosso projeto não será afetado.
O próximo da lista é Liskov Substitution, sem dúvidas é o princípio mais complexo dos outros 5. Se uma classe A é um subtipo da classe B, então nos deveriamos ter capacidade para substituir B com A sem implicar no comportamento do nosso programa.
Indo direto ao código:
interface Carro
{
public function ligarMotor();
public function acelerar();
}
No código acima, definimos uma simples interface chamada Carro com dois métodos que todos os carros deveriam cumprir: ligar o motor e acelerar.
class Automovel implements Carro
{
private $motor;
public function __construct(Motor $motor)
{
$this->motor = $motor;
}
public function ligarMotor()
{
$this->motor->ligar();
}
public function acelerar()
{
$this->motor->vel(1000);
}
}
Como nosso código descreve, nós temos um motor que podemos ligar e podemos acelerar, através de métodos de uma classe Motor que passamos por injeção de dependência no construtor. Mas, e se quisermos adicionar um carro elétrico ?
class CarroEletrico implements Carro
{
public function ligarMotor()
{
throw new Exception('Nós não temos um motor');
}
public function acelerar()
{
// Essa aceleração é uma loucura
}
}
Jogando um carro sem motor dentro de tudo, estamos mudando inerentemente o comportamento do nosso programa. Esta é uma violação flagrante da substituição de Liskov e é um pouco mais difícil de corrigir do que nossos dois princípios anteriores.
Uma possivel solução deveria ser trabalhar novamente nosso modelo dentro da interface que levam em consideração o estado sem motor do nosso carro.
O "I" em SOLID refere-se a interface segregation, e significa que interfaces maiores devem ser divididas em interfaces menores. Ao fazer isso, garantimos que a implementação de classes só precisam se preocupar apenas com métodos que são do interesse dela.
Vamos começar com uma interface tratadora de funções genéricas.
interface Generic
{
public function colocar();
public function guardar();
public function mover();
// ...
}
É bem comum colocar-mos muitos métodos dentro de uma classe, mesmo que não usamos. Com isso nossa classe ficará muito grande e não temos escolha a não ser ir implementando novos métodos.
Vamos consertar isso dividindo nossa longa interface em três interfaces separadas.
interface GenericColocar
{
public function colocar();
// ...
}
interface GenericGuardar
{
public function guardar();
// ...
}
interface GenericMover
{
public function mover();
// ...
}
Agora, vamos implementar apenas os métodos que importam pra nós.
class Generic implements GenericColocar, GenericGuardar
{
public function guardar()
{
// ..
}
public function colocar()
{
// ..
}
// ...
}
Poderíamos até mesmo dividir nossa classe CarroRaro de nossos exemplos anteriores para usar a segregação de interfaces da mesma forma. Implementando uma interface de um modelo de carro com uma unica marca.
O princípio de inversão de dependência refere-e ao desacoplamento de módulos de software. Dessa forma, em vez de módulos de alto nível dependendo de módulos baixo nível, ambos dependerão de abstrações.
Para que uma classe dependa de uma abstração e não de uma implementação.
Vamos exemplificar.
class Email
{
public function enviar($mensagem)
{
// ...
}
}
class Notificacao
{
public function __construct()
{
$this->mensagem = new Email;
}
public function enviar($mensagem)
{
$this->mensagem->enviar($mensagem)
}
}
Neste exemplo, temos o que chamamos de acoplamento e uma dependência da classe Notificacao cria uma instância da classe Email dentro dela. E o método enviar faz a utilização da classe Email para enviar a notificação por e-mail. Isso fere o princípio da inversão da dependência, porque não desenvolvemos a classe Notificacao para uma abstração, e sim para uma implementação já que a classe Email implementa a lógica para o envio do e-mail.
Vamos resolver isso.
interface MensagemInterface
{
public function enviar($mensagem);
}
class Email implements MensagemInterface
{
public function enviar($mensagem)
{
// lógica
}
}
class Notificacao
{
public function __construct(MensagemInterface $mensagem)
{
$this->mensagem = $mensagem;
}
public function enviar($mensagem)
{
$this->mensagem->enviar($mensagem)
}
}
Agora desacoplamos a classe Email da classe Notificacao, estamos trabalhando com a abstração MensagemInterface, para Notificacao não importa qual classe você está usando, e sim, que ela implemente a interface MensagemInterface porque sabemos que ela vai ter o método enviar que precisamos. Isso também permite que a classe Notificacao use outras classes que implementem a interface MensagemInterface.
Com isso, esclarecemos as noções básicas para usar os princípios SOLID na sua aplicação. No mundo real, nem sempre conseguimos usar todos os princípios, eles são como guias para nos ajudar a escrever um código mais eficiente e organizado.