Design Pattern: SOLID

Exemplificando cada letra =)

Introdução

Agora que já falamos sobre alguns princípios básicos de projetos, vamos dar uma olhada em cinco deles que são mais conhecidos como os príncipios SOLID.

O SOLID é uma sigla em inglês para cinco princípios de projetos destinados a fazer dos projetos de software algo mais compreensivo, plexível e sustentável.

Mas devemos ter cautela ao usar esses princícios, pois o uso sem o devido cuidado pode dar muita dor de cabeça. O custo de aplicar esses princípios na arquitetura pode deixa-lo muito mais complicado do que deveria ser.

Será que existe algum produto de sucesso no mercado que use todos eles ao mesmo tempo? 🤔

Agora vamos fazer um deep dive em cada letrinha desse tal de SOLID.

S → Single Responsibility Principle ou Princípio de responsabilidade única

Uma classe deve ter apenas uma razão para ser alterada.

Como o próprio nome já diz, o objetivo é fazer com que a classe seja responsável por apenas uma única parte da funcionalidade fornecida pelo seu projeto, faça essa responsabilidade ser totalmente encapsulada pela classe.

O objetivo principal desse princípio é reduzir a complexidade, tanto do entendimento, quanto de manutenção. Os verdadeiros problemas só começam a aparecer quando um projeto começa a crescer e ser modificado com frequência. Em certo ponto, as classes se tornarão tão grandes que vc nem vai lembrar mais as regras que ela possui.

Ao tentar realizar qualquer correção ou implementar uma feature, a navegação pelo código se torna mais um engatinhar, pois terá que olhar quase que com uma lupa para olhar cada classe para encontrar certas coisas dentro do código. As vezes o número de entidades é tão grande, a regra é tão complexa, que vc se sentirá perdido e sem controle sobre o código.

Análise as responsabilidades de sua classe, pois quanto mais coisas ela faz, maior a chance de causar um efeito colateral.

Se vc sente que está complicado, que ao mexer num ponto pode quebrar outro, talvez seja a hora de dividir em algumas classes em partes menores, de contexto reduzido.

Vamos fazer um exemplo e ajustar conforme o princípio:

<?php
class Cart 
{
    public function checkout(){/*Do Code*/}
    public function calculateTotalSum(){/*Do Code*/}
    public function getItems(){/*Do Code*/}
    public function calculatePromotion(){/*Do Code*/}
    public function calculateShipping(){/*Do Code*/}
    public function showOrder(){/*Do Code*/}
}

Temos uma classe Cart com todas essas responsabilidades, que vai desde o checkout até o mostrar o pedido no final, isso fere o princípio pois ele tem muitas responsabilidades e não queremos isso.

Vamos refatorar deixando cada classe com seu papel:

<?php
class Cart 
{
    public function checkout(){/*Do Code*/}
    // Other functions
}

class Promotion 
{
    public function getPromotions(){/*Do Code*/}
    public function calculatePromotion(){/*Do Code*/}
}

class Shipping
{
    public function getAddress(){/*Do Code*/}
    public function calculateShipping(){/*Do Code*/}
}

class Order
{
    public function getItems(){/*Do Code*/}
    public function showOrder(){/*Do Code*/}
}

Agora cada classe está cuidado de sua responsabilidade.

O → Open/Closed Principle ou Princípio aberto/fechado

As classes devem ser abertas para extensão, mas fechadas para modificação.

O objetivo desse princípio é prevenir que o código existente quebre quando vc implementa novas funcionalidades.

Uma classe é aberta quando é possível extende-la, criar subclasses e ser livre para modificar como quiser, criar novos métodos ou campos e até mesmo sobrescrever o comportamento padrão. A classe é fechada (ou completa) se ela estiver 100% pronta para uso em outras, sendo assim ela está claramente definida com suas interfaces e não sofrerá modificações em seu comportamento.

Os termos aberto/fechado não são exclusivos, uma classe pode ser tanto aberta (extensão) e fechada (modificação) ao mesmo tempo.

Se a sua classe foi desenvolvida, testada, passou por todo o fluxo de revisão e já está sendo usada pela aplicação, tentar mexer no código pode ser arriscado. Ao invés de alterar o código da classe diretamente, crie subclasses e sobrescreva as partes da classe original que vc quer que tenha um comportamento diferente. O seu objetivo será alcançado sem correr o risco de quebrar os códigos que já estão usando a classe original.

Se vc ver um bug na classe mãe vc deve corrigi-lo nela e não criar uma subclasse para isso, a filha não deve se responsabilizar pelos problemas da mãe.

Lembra do gato que só come salmão (se não sabe do que eu to falando te convido a ler o artigo anterior), digamos que ele começa a experimentar outro tipo de comida, agora ele gosta de atum, o código ficará dessa forma:

<?php
class Salmon {/*Do Code*/}

class Atum {/*Do Code*/}

class Cat 
{
    public function eat( $food ) : string
    {
        if ($food instanceof Atum) {
            return 'The cat is was fed with atum';
        } 
        if ($food instanceof Salmon) {
            return 'The cat is was fed with salmon';
        } 
    }
}

$cat = new Cat;
echo $cat->eat(new Salmon);
echo "<br>";
echo $cat->eat(new Atum);

// Output
// The cat is was fed with salmon
// The cat is was fed with atum

Agora iremos aplicar o princípio:

<?php
abstract class Food {/*Do Code*/}

class Salmon extends Food {/*Do Code*/}

class Atum extends Food {/*Do Code*/}

class OtherFood extends Food {/*Do Code*/}

class Cat 
{
    public function eat( Food $foodName ) : string
    {
        return 'The cat was fed with: '.get_class($foodName);
    }
}

$cat = new Cat;
echo $cat->eat(new Salmon);
echo "<br>";
echo $cat->eat(new Atum );
echo "<br>";
echo $cat->eat(new OtherFood);

// Output
// The cat is was fed with salmon
// The cat is was fed with atum

L → Liskov Substitution Principle ou Princípio de substituição de Lisk

Quando extenter uma classe, devemos ser capazes de passar objetos da subclasse no lugar de objetos da classe mãe sem quebrar o código cliente.

Mas oq isso quer dizer? Significa que a subclasse deverá ser compatível com o comportamento da superclasse. Sempre que sobrescrever um método, primeiramente analise o comportamento base ao invés de implementar algo completamente diferente.

O princípio da substituição é um conjunto de checagens que ajudam a prever se uma subclasse permanece compatível com o código da superclasse. O conceito é bem rígido quando o projeto são bibliotecas e frameworks, pois suas classes serão usadas por outras pessoas cujo código vc não terá acesso para acessar ou mudar.

Os outros princípios são amplamente abertos a interpretação, já esse tem um conjunto de requerimentos formais para as subclasses, e especificamente para os seus métodos, vamos ver cada um deles com mais detalhes.

Os métodos das subclasses devem ter seus parâmetros similares ou mais abstratos q que os da superclasse.

Vamos ao exemplo, vc tem uma classe que implementa o método fed(Cat $cat) , o código cliente sempre passa objetos do tipo Cat.

Você resolve estender essa classe para alimentar qualquer animal usando fed(Animal $animal).

<?php
abstract class Food 
{
    public function fed(Cat $cat ): string
    {
        return 'The cat was fed';
    }
}

class Salmon extends Food 
{
    public function fed(Animal $animal ): string
    {
        return 'The animal was fed';
    }
}

Os retornos das subclasses devem ser do mesmo tipo ou serem iguais aos retornos da superclasse.

Vamos ao exemplo, vc tem uma classe que implementa o método buyACat(Cat $cat) : Cat, o código cliente sempre espera como retorno objetos do tipo Cat.

Você resolve estender essa classe para poder vender uma raça específica de gatos buyACat(PersianCat $persianCat) : PersianCat

<?php
abstract class SellACat 
{
   public function buyACat(): Cat
   {
       return New Cat;
   }
}

class SellAPersianCat extends Food 
{
    public function buyACat(): PersianCat
    {
        return New PersianCat;
    }
}

Um exemplo que NÃO deve ser seguido vem das linguagens com tipagem dinâmica: o método base retorna um boleano, mas o método sobrescrito retorna uma string.

Um método de uma subclasse NÃO deve lançar tipos de excessões que não são esperados que o método base lançaria.

Ou seja, novamente dizendo, os tipos dessas excessões devem ser SEMELHANTES ou de SUBTIPO daqueles que o método base já é capaz de lançar.

Caso vc lançe algo que não é contemplado pelo bloco try-catch do cliente, a excessão inesperada pode escapar, não ser capturada e o código cliente irá quebrar, afetando assim toda aplicação.

Uma subclasse não deve alterar as pré-condições ou fortalece-las.

Exemplo: Se o método base de uma classe tem como parâmetro um número inteiro. Caso uma subclasse sobrescreva essa método e precisa que o valor seja apenas positivo, lançando uma excessão caso seja negativo, isso torna a pré-condição forte e caso o cliente passe a usar os objetos fornecidos pela subclasse, o código que antes funcionava perfeitamente com números negativos, agora irá quebrar.

Uma subclasse não deveria enfraquecer pós-condições.

Exemplo: Você tem no seu método base que trabalhe com uma base de dados. O método base sempre fecha as conexões ao retornar o valor desejado ou ao final da escrita.

Vc estendeu essa classe mas resolveu sobrescrever esse método alterando seu comportamento para deixar a conexão aberta para alguma reutilização futura. O código cliente pode não saber nada sobre isso, pois ele espera pelo comportamento padrão, que é o método encerrar todas as conexões, poluindo o sistema com conexões fantasmas para a base de dados.

As constantes de uma superclasse não devem ser alteradas.

Essa regra pode ser a mais fácil de se quebrar pois pode ser que vc não entenda ou não perceba todas as constantes de uma classe muito complexa. Sendo assim, o modo mais seguro é incluir novos campos e métodos ao estender a superclasse. e não mexer com qualquer coisa existente na superclasse, nem sempre isso é viável no mundo real.

Uma subclasse não deve mudar diretamente os valores de varíaveis privadas da superclasse.

Exemplo: Atributos privados, só devem ser alterados com métodos setters.

<?php
abstract class Food 
{
    private $nutricion;

    public function setNutricion($nutricionValue)
    {
        $this->nutricion = $nutricionValue;
    }
}
class Salmon extends Food 
{
    public function __construct()
    {
        //Errado
        $this->nutricion = 10;

        //Correto
        $this->setNutricion(10);
    }
}

I → Interface Segregation Principle ou Princípio de segregação de interface

A classe não deve ser forçada a depender de métodos que não usam.

Ao criar interfaces, se preocupe em reduzi-las o suficiente para que classe não implemente algo só por implementar, certifique que será usado.

Esse princípio sugere que vc deve quebrar interfaces que são muito genéricas para mais específicas. Os clientes devem implementar oq realmente faz sentido ser implementado. Caso contrário, se acontecer uma mudança na interface genérica, o código cliente que nem usa o método será afetado pela alteração, podendo até quebrar a aplicação.

A herança permite uma e apenas uma superclasse, mas não existe limitação para interfaces que uma classe pode implementar ao mesmo tempo. Assim não existe a necessidade de uma só interface com toneladas de métodos que não fazem sentido para todos. Caso isso aconteça, faça um refinamento dessa interface e quebre-a em menones. É possível que vc implemente TODAS em uma única classe, mas a classe que precisa de apenas UMA, ficará bem melhor implementando apenas essa.

Exemplo do que não fazer:

<?php
interface Roles
{
    public function frontend();
    public function backend();
    public function devops();
    public function teamManager();
}

class BackEnd implements Roles
{
    public function backend(){/*Do some code*/}

    public function frontend(){/*I'm not a front end developer*/}

    public function devops(){/*I'm not a devops*/}

    public function teamManager(){/*I'm not a Team Manager*/}
}

Temos uma classe BackEnd que só fará a função backend(), mas por ele implementar a interface Roles, ele é obrigado a implementar todas as outras funções mesmo sem necessidade.

Jeito certo de resolver isso:

<?php
interface TMInterface
{
    public function teamManager();
}

interface FrontEndInterface
{
    public function frontend();
}

interface BackEndInterface
{
    public function backend();
}

interface DevOpsInterface
{
    public function devops();
}

class BackEnd implements BackEndInterface
{
    public function backend(){/*Do some code*/}
}

class FrontEnd implements FrontEndInterface
{
    public function frontend(){/*Do some code*/}
}

Aqui quebramos a interface Roles para deixar mais segmentado, caso vc tenha um BackEnd que seja devops por exemplo, basta implementar essa interface, assim fica muito melhor e só será implementado o necessário.

Assim como nos outros princípios, vc pode acabar exagerando, não divida demais uma interface que já é específica, encontre um equilíbrio, pois é importante lembrar que quanto mais interfaces, mais complexo fica o seu código.

D → Dependency Inversion Principle ou Princípio de inversão de dependência

Classes de alto nível não deveriam depender de classes de baixo nível, ambas devem depender de abstrações. As abstrações não devem depender de detalhes. Detalhes devem depender de abstrações.

Num projeto de software, podemos distinguir dois níveis de classe:

  • Classes de baixo nível ⇒ Implementam operações básicas, gravam em disco, conexão a banco de dados, etc.
  • Classes de alto nível ⇒ Implementam a regra de negócio complexa que direcionam as classes de baixo nível para fazerem algo.

Em alguns projetos é comum ver o início da implementação ser nas classes de baixo nível e só depois de tudo definido seguir nas de alto nível. Isso é muito comum em protótipos, pois não sabemos oq será possivel realizar em alto nível, sendo que as classes de baixo nível ainda não foram implementadas ou ainda não estão claras. Com essa abordagem, classes da regra de negócio tendem a ter uma certa dependência das classes de baixo nível.

Esse princípio sugere a troca dessas dependências.

Mas como faremos isso? Vejamos.

  1. Para o início, é necessário desenvolver as interfaces para as operações de baixo nível que as de alto nível dependem, de preferência o mais próximo da regra de negócio. Por exemplo, a lógica de negócio deve chamar um método insertDataBase($query) e não uma série de métodos connectDatabase(), insert($query) e por fim closeConnection($connection) . Essas interfaces contam como alto nível.
  2. Agora vc pode fazer a classe de alto nível depender dessas interfaces, ao invés de classe concretas de baixo nível. Essa abstração ajudará na flixibilidade do seu código.
  3. As classes dependendo dessa interface, elas se tornam dependentes a nível da regra de negócio, fazendo assim a inversão da depêndencia.

Exemplo do princípio usando o (Spoiler) desing pattern Repository.

<?php
interface DataBaseInterface
{
    public function connect($serverName, $userName, $password);
    public function closeConnection($connection);
}

abstract class DataBaseAbstract
{
    public $dbConnection;
    public function __construct() 
    {
        $this->dbConnection = $this->connect(config('database.server'), config('database.user'), config('database.password'));
    }
}

class MySQL extends DataBaseAbstract implements DataBaseInterface
{
    public function connect($serverName, $userName, $password){/* Connect */}

    public function closeConnection($connection){/* Disconnect */}
}

class Mongo extends DataBaseAbstract implements DataBaseInterface
{
    public function connect($serverName, $userName, $password){/* Connect */}

    public function closeConnection($connection){/* Disconnect */}
}

class UserController
{
    private $dataBase;
    public function __construct(DataBaseInterface $db) 
    {
        $this->dataBase = $db;
    }
        // Do a insert.
}

A classe UserController não precisa se preocupar com qual banco de dados a aplicação irá utilizar. Dessa forma, não estamos mais violando o princípio, ambas as classes estão desacopladas e dependendo de uma abstração.

Além disso, estamos favorecendo a reusabilidade do código e também estamos respeitando o princípio S e O.

Um coisa que vc deve ter em mente, Inversão de Dependência não é a mesma coisa que Injeção de Dependência. A Inversão de Dependência é um conceito e a Injeção de Dependência é um design pattern.