DEV Community

luanloose
luanloose

Posted on

Design Pattern: Introdução e características

Introdução

Design Patterns (padrões de projeto) são soluções típicas para problemas comuns, são como plantas baixa de um prédio por exemplo. Um padrão de projeto não é algo que pode ser copiado para dentro do seu contexto, não funciona como exemplos de funções de documentações, libs ou aquele código maroto do stackoverflow 😝. Pois o padrão é um conceito geral a ser usado para resolver um problema particular.

Temos 3 tipos de padrões:

  1. Criacionais: Fornecem criação de objetos que aumentam a flexibilidade e a reutilização de código.
  2. Estruturais: Mostram como é montado objetos e classes em estruturas mais robustas, mantendo-a flexível e eficiente.
  3. Comportamentais: Cuidam da comunicação eficiente e da assinalação entre objetos.

Por que aprender sobre isso?

É possível trabalhar "tranquilamente" como desenvolvedor por anos sem saber nada sobre padrões, muita gente segue assim inclusive, masssss de um jeito ou de outro, no dia a dia vc acabará implementando um dos padrões existentes sem saber.

Sendo assim, pq não aprender sobre eles?

Os padrões de projeto resolvem diversos problemas comuns em projetos de software, pense neles como se fosse uma caixa de ferramentas, vc olha o problema, abre a caixa, analisa qual ferramenta vai precisar no momento, pega e usa.

Simples, não?

Além disso, eles propõem uma definição para tais implementações, vc se depara com um código, vê que se parece com algum padrão e diz, é possível resolver isso usando Strategy, caso o colega conheça o padrão, logo ele entenderá a sua sugestão.

Se não entender, é uma ótima oportunidade para ensinar e passar o conhecimento.

Características de um bom projeto

Antes de seguir vamos comentar sobre algumas características que são almejadas nos nossos projetos e algumas coisas que devemos evitar.

  • Reutilização de código

    É um dos modos mais comuns para se reduzir custos no desenvolvimento. O objetivo é simples: Ao invés de copiar e colar o mesmo código dentro do mesmo projeto ou desenvolver algo do zero sempre, por que não reutilizar códigos já existentes nos seus novos projetos?

    O objetivo parece muito simples, mas na prática, fazer um código que já existe e que funciona sozinho, funcionar em outro contexto, exige um esforço adicional.

    Para conseguir reutilizar com êxito seus códigos, deve-se evitar dependências de classes concretas ao invés de interfaces e operações hardcoded. Essas coisas reduzem a flexibilidade do seu código e deixa ele cada vez mais difícil a sua reutilização.

  • Extensibilidade

    Seu código está pronto para novos componentes? Novas regras?

    Tendo um ecommerce, o cliente percebe a necessidade de adicionar um novo método de pagamento, seu código deve estar preparado e componentizado para suportar tais mudanças, caso contrário ele quebrará. Uma coisa temos que ter em mente, a única constante de um desenvolvedor é a mudança, esteja preparado para ela, implemente algo hoje para que não quebre se for mudado amanhã.

    Existe um lado bom nisso: Se alguém pede novos recursos na sua aplicação, significa que ela ainda é importante para a empresa.

Estamos falando sobre um bom projeto, mas o que é um bom projeto? Como podemos deixar nossa arquitetura flexível, estável, escalável e de fácil entendimento?

Essas perguntas são boas, mas as respostas dependem de cada contexto. A boa notícia é que existem vários princípios universais de projeto de software que podem ajudar nessas respostas.

Encapsulamento

Identifique tudo que é variável e separe dos que permanecem os mesmos.

O objetivo desse princípio é minimizar o efeito causado por mudanças.

Imagine o seguinte, temos um supermercado, ao efetuar um pagamento, ele é recusado, oq vai acontecer com os produtos no caixa? Oq acontece com o cliente? Seu código deve estar encapsulado para poder tratar essas excessões. Você pode isolar cada parte que varia em módulos independentes, protegendo o restante da aplicação contra os efeitos indesejados, dessa forma é possível tratar os erros lançados e evitando complicações, como por exemplo um estoque que não volta ao seu estado original quando o pagamento não é efetuado.

Quanto menos tempo vc gasta fazendo mudanças, mais tempo ganha para implementar novas funcionalidades.

Encapsulamento nível método

Bom, suponha que vc tenha um ecommerce com o método checkout nele contém toda a regra de calcular os valores totais de acordo com o meio de pagamento, calcula frete, verifica se tem promoção e fecha o pedido tudo no mesmo código, como por exemplo:

<?php
class Cart {
    use Promotion;

    public function checkout(){

        $products = [1, 2, 3, 4];
        foreach ($arr as &$value) {
            $totalValue += $value;
        }

        // cálculo de promoção
        // cálculo de frete
        // fechar pedido
    }
}
Enter fullscreen mode Exit fullscreen mode

Caso alguma regra dessas mude a função ficará cada vez mais extensa, mais complexa para manutenção e refatoração, vai precisar de muitos testes para garantir que nada quebre, a melhor estratégia é extrair certos trechos para contextos menores, deixando dessa forma:

<?php
class Cart {
    public function checkout(){

        $products = [1, 2, 3, 4];
        foreach ($arr as &$value) {
            $totalValue += $value;
        }

        $this->calculatePromotion();
        $this->calculateShipping();

        // fechar pedido
    }

    function calculatePromotion(){
        // cálculo de promoção
    } 

    function calculateShipping(){
        // cálculo de frete
    }
}
Enter fullscreen mode Exit fullscreen mode

Agora usando dessa forma, cada função ficou isolada da principal, caso alguma se torne mais complexa, vc pode facilmente criar uma classe, a refatoração se torna menos onerosa.

Encapsulamento nível de classe

Seu método que antes era simples e tinha apenas uma responsabilidade, vem ganhando mais e mais recursos, se tornando mais complexo, e assim desfocando a responsabilidade primária da classe a qual ele pertence. Extrair tudo para uma nova classe pode deixar as coisas mais claras e simples.

  • Nosso exemplo se encontra dessa forma

1.jpeg

  • Agora faremos um objeto especial receber todo o trabalho relacionado a promoções.

2.jpeg

  • O código ficará algo assim:

    <?php 
    class Cart {
        use Promotion;
    
        public function checkout(){
    
            //calcula sub total
            $totalValue = $subTotal + Promotion::calculatePromotion(products);
    
            $this->calculateShipping();
            //fecha pedido
        }
    }
    

O calculo das promoções ficará abstraído dentro da classe Promotion.

Desenvolva interfaces

Nunca dependa de classes concretas e sim abstrações.

Seu código precisa ser o mais flexível possível, para isso é preciso que seja possível extendê-lo facilmente sem que nada quebre no código existente. Para garantir que essa afirmação é correta vamos para mais um exemplo, saindo do contexto de ecommerce, usaremos gatos para simplificar.

Quem ai não ama gatinhos não é mesmo?

Se não ama, pode se retirar por favor, BRINKS

Um gato que come qualquer comida é mais flexível e fácil de satisfazer do que um que come apenas salmão, gato chique esse ein. Você ainda pode alimentar o primeiro gato com salmão para agrada-lo vez ou outra, mas não poderá alimentar o segundo com atum por exemplo quando chegar no fim do mês quando a grana esta curta, o cardápio do primeiro pode ser estendido para qualquer outra comida, sendo mais fácil para alimenta-lo.

  • Diagrama demonstrando os dois gatos

3.jpeg

Mas como eu deixo meu código mais flexível??

Existem alguns meios de configurar uma colaboração entre objetos:

  1. Determine o que exatamente um objeto precisa do outro, quais métodos ele irá executar.
  2. Descreva esses métodos em uma nova interface ou classe abstrata.
  3. Na classe que é um dependência, implemente essa interface.
  4. Agora faça a segunda classe ser dependente dá interface ao invés da classe concreta. Ainda será possível fazer ela funcionar com objetos da classe original, só que a conexão é muito mais flexível.
  5. Gato 1

    <?php
    class Salmon {
    
        public $nutrition;
    
        public function __construct()
        {
                $this->nutrition = 10;
        }
    }
    
    class Cat {
    
        public function eat( Salmon $salmon ) : string
            {
            return 'The cat is was fed with nutrition: '.$salmon->nutrition;
        }
    
    }
    
    $cat = new Cat;
    echo $cat->eat(new Salmon);
    
    // Output
    // The cat was fed with nutrition: 10
    
    
  • Gato 2

    <?php
    abstract class Food {
    
        public $nutricion;
    
    }
    
    class Salmon extends Food {
    
        public function __construct()
        {
                $this->nutricion = 10;
        }
    
    }
    
    class Cat {
    
        public function eat( Food $foodName ) : string
            {
            return 'The cat is was with nutricion: '.$foodName->nutricion;
        }
    
    }
    
    $cat = new Cat;
    echo $cat->eat(new Salmon);
    
    // Output
    // The cat was fed with nutricion: 10
    

Antes e depois de extrair a interface, o segundo código é mais flexível que o primeiro, pois quaisquer novas comidas serão "comidas" pelo gato e não somente uma.

Ufa, agora sim poderei economizar um pouco com meu gato.

Prefira composição sobre herança

A herança é provavelmente a maneira mais comum de se reutilizar código, é a mais óbvia né, temos duas classes com códigos em comum, a primeira coisa que vem na mente, vamos criar uma classe base, pegamos o código comum, joga nela e pronto, extende nas outras duas, sucesso.

Porém, a herança vem com um lado ruim que só é aparente quando seu projeto está com mais classes e mudar tudo fica muito difícil, aqui temos uma lista desses problemas:

  • Uma subclasse tem que implementar TODOS os métodos que possuem na interface da superclasse mesmo que não seja usado.
  • Quando sobrescrever métodos, vc precisa se certificar que o novo comportamento é compatível com o base, pois a subclasse pode ser passados para qualquer código do tipo da superclasse e vc não quer nada quebre.
  • A herança quebra o encapsulamento da superclasse, detalhes internos da classe mãe ficam disponíveis para a subclasse.
  • As subclasses estão firmemente acopladas as superclasses, caso tenha alguma mudança em uma superclasse pode gerar um efeito colateral e quebrar o código das subclasses.
  • Tentar reutilizar código através da herança pode levar a criação de uma cadeia de heranças paralelas, a herança acontece em uma dimensão.

Existe uma alternativa para a herança que tem o nome de composição. A herança representa a relação de "pipoca é um alimento", "cachorro é um animal" a composição representa a relação "cachorro tem 2 olhos", "pipoca tem temperos".

Sempre pense no seu código o mais desacoplado possível.

É isso galera, nos vemos no próximo artigo.

Discussion (0)