Ir ao conteúdo

Padrão de Projeto Strategy em PHP com exemplo

Sem dúvidas, o Padrão de Projetos Strategy ou Strategy Pattern é um dos padrões mais utilizados pela comunidade de programadores. Muitos utilizam este pattern, através de frameworks, às vezes sem se darem conta, tudo por conta da simplicidade de sua implementação.

O Padrão Strategy permite definir novas operações sem alterar as classes dos elementos sobre os quais opera. Ou seja, ele encapsula um algoritmo em um objeto e através de uma interface, conseguimos fazer a troca(intercâmbio) entre os algoritmos concretos que implementam os métodos definidos na interface.

Diagrama UML do Padrão de Projeto Strategy
Diagrama UML do Padrão de Projeto Strategy

Para você contextualizar melhor, vamos pensar em um problema e gerar uma solução através do uso do Padrão Strategy. No artigo Padrão de Projeto Factory Method em PHP com exemplo, criamos uma Fábrica de Instrumentos Musicais, porém, acabamos criando um Method Factory que testava o tipo de instrumento (acústico ou elétrico). Só que  neste exemplo, criei uma dependência forte, onde eu fiz uma composição e testei seu tipo através do uso de um switch case (uma alternativa aos IFs).

Qual o problema de fazer desta maneira? Imagine que você quer que essa fábrica produza Piano, Violino ou uma Bateria… Cada nova implementação, precisaria modificar o comportamento interno desta classe. Com isso, estaríamos ferindo o Open-Closed Principle do SOLID

No exemplo do Method Factory estava assim:

Exemplo de Factory Method utilizando Switch
Exemplo de Factory Method utilizando Switch

Podemos resolver este problema utilizando o Strategy desta forma:

Exemplo de Factory Method com uso do Strategy
Exemplo de Factory Method com uso do Strategy

O exemplo completo com as demais classes concretas e testes da adaptação do Padrão Strategy junto com o Padrão Method Factory, você encontra no meu Github: https://github.com/growthdev-repo/design-patterns

Como você pode notar, neste exemplo tem código “sobrando”. Deixei desta forma, como forma didática. Mas não se preocupe, vamos fazer um exemplo completo na prática no decorrer deste artigo.

Qual a vantagem do uso do Padrão Strategy?

O principal benefício é a reutilização de partes do algoritmo com funcionalidades comuns, de modo que nossos algoritmos, em classes Strategy, possam variar independentemente do seu contexto. Ou seja, conseguimos fazer diferentes implementações do mesmo comportamento.

O padrão Strategy também é uma excelente opção em detrimento ao uso de Herança. Quanto mais desacoplado o seu código for, melhor ele será. Você consegue variar seu código de efeitos colaterais, por exemplo, sem ter que modificar uma classe Mãe, como acontece em alguns casos com herança.

Resumidamente, o Padrão de Projetos Strategy respeita os seguintes princípios:

  • SOLID: Explicitamente está em acordo com o OCP, Open-Closed Principle, já explicado aqui.
  • Object Calisthenics: A motivação é a aplicação dos princípios SOLID. São 9 regras focadas em manutenibilidade, legibilidade, testabilidade e compreensão do código 
  • KISS: Siglas para “Keep It Simple, Stupid”. É um princípio de design que afirma que projetos e/ou sistemas devem ser tão simples quanto possível.
  • DRY: Siglas para “Don’t Repeat YourSelf”. É um importante princípio que procura reduzir a duplicação de código e as consequências de sua prática.

Desvantagem do uso do Padrão Strategy

Sem dúvidas, uma grande desvantagem do padrão Strategy, é que aumenta bastante o número de classes em nossos projetos. Dependendo da forma como você utilizar, vai ferir o princípio KISS e ainda de quebra o princípio YAGNI (You aren’t gonna need it).

“Sempre implemente funcionalidades quando você realmente precisar delas, e nunca quando você prever que vai precisar delas”.

Aplicando o Padrão de Projeto Strategy passo a passo

Existem diversos tipos de problemas que podemos resolver utilizando o padrão Strategy. Vamos direto para um problema específico e na sequência, vamos gerar sua solução em código.

Imagine que você precisa implementar um Gateway de Pagamento que vai aceitar diversos método de pamento, como: Crédito, Débito ou em Dinheiro. Só que dependendo do método, vai incidir um pequeno desconto. Por exemplo, no dinheiro 10%, no Débito 5% e no crédito, não vai ter desconto.

Como você faria esta implementação utilizando o Strategy Pattern? 

Primeiro vamos definir um contrato utilizando uma interface onde as classes que as implementar, terão que obrigatoriamente implementar o seu método pay:

<?php

declare(strict_types=1);

namespace Growthdev\DesignPatterns\Behavioral\Strategy\Payment;

interface PaymentMethodStrategy
{
    public function pay(float $amount): float;
}

Agora vamos definir as classes que servirão como modelos para representação dos métodos de pagamento:

<?php

declare(strict_types=1);

namespace Growthdev\DesignPatterns\Behavioral\Strategy\Payment;

class CashPaymentMethod implements PaymentMethodStrategy
{
    private const DISCOUNT_PERCENT = 0.10; // 10%

    public function pay(float $amount): float
    {
        return $amount - ($amount * self::DISCOUNT_PERCENT);
    }
}
<?php

declare(strict_types=1);

namespace Growthdev\DesignPatterns\Behavioral\Strategy\Payment;

class DebitCardPaymentMethod implements PaymentMethodStrategy
{
    private const DISCOUNT_PERCENT = 0.05;  // 5%

    public function pay(float $amount): float
    {
        return $amount - ($amount * self::DISCOUNT_PERCENT);
    }
}
<?php

declare(strict_types=1);

namespace Growthdev\DesignPatterns\Behavioral\Strategy\Payment;

class CreditCardPaymentMethod implements PaymentMethodStrategy
{
    private const DISCOUNT_PERCENT = 0.00; // 0%

    public function pay(float $amount): float
    {
        return $amount - ($amount * self::DISCOUNT_PERCENT);
    }
}

Por fim, vamos criar nosso contexto de processamento de pagamento:

<?php

declare(strict_types=1);

namespace Growthdev\DesignPatterns\Behavioral\Strategy\Payment;

final class PaymentProcessor
{
    private PaymentMethodStrategy $paymentMethod;
    private float $amount;

    public function __construct(PaymentMethodStrategy $paymentMethod)
    {
        $this->paymentMethod = $paymentMethod;
    }

    public function processPayment(float $amount): void
    {
        $this->amount = $this->paymentMethod->pay($amount);
    }

    public function getAmount(): float
    {
        return $this->amount;
    }
}

Criei apenas 3 testes para você ver a aplicação deste pattern:

<?php

declare(strict_types=1);

namespace Growthdev\DesignPatterns\Tests\Behavioral\Strategy\Payment;

use Growthdev\DesignPatterns\Behavioral\Strategy\Payment\CashPaymentMethod;
use Growthdev\DesignPatterns\Behavioral\Strategy\Payment\CreditCardPaymentMethod;
use Growthdev\DesignPatterns\Behavioral\Strategy\Payment\DebitCardPaymentMethod;
use Growthdev\DesignPatterns\Behavioral\Strategy\Payment\PaymentProcessor;
use PHPUnit\Framework\TestCase;

final class PaymentProcessorTest extends TestCase
{
    public function testCanProcessPaymentWithCashPaymentMethod(): void
    {
        $paymentProcessor = new PaymentProcessor(new CashPaymentMethod());
        $paymentProcessor->processPayment(100.00);

        $this->assertEquals(90.00, $paymentProcessor->getAmount());
    }

    public function testCanProcessPaymentWithDebitCardPaymentMethod(): void
    {
        $paymentProcessor = new PaymentProcessor(new DebitCardPaymentMethod());
        $paymentProcessor->processPayment(100.00);

        $this->assertEquals(95.00, $paymentProcessor->getAmount());
    }

    public function testCanProcessPaymentWithCreditCardPaymentMethod(): void
    {
        $paymentProcessor = new PaymentProcessor(new CreditCardPaymentMethod());
        $paymentProcessor->processPayment(100.00);

        $this->assertEquals(100.00, $paymentProcessor->getAmount());
    }

}

Veja a mesma implementação sem o padrão Strategy

Reaproveitando os códigos, vamos criar apenas um Payment Processor sem o uso do Strategy, sem modificar também  as classes que representam os métodos de pagamento. Com este exemplo vai ficar claro para você a importância de conhecer sobre Padrões de Projetos:

<?php

declare(strict_types=1);

namespace Growthdev\DesignPatterns\Behavioral\Strategy\Payment;

final class PaymentProcessorWithoutStrategy
{
    public const PAYMENT_TYPE_CREDIT_CARD = 'credit_card';
    public const PAYMENT_TYPE_DEBIT_CARD = 'debit_card';
    public const PAYMENT_TYPE_CASH = 'by_cash';

    private string $paymentMethod;
    private float $amount;

    public function __construct(string $paymentMethod)
    {
        $this->paymentMethod = $paymentMethod;
    }

    public function processPayment(float $amount): void
    {
        switch ($this->paymentMethod) {
            case self::PAYMENT_TYPE_CREDIT_CARD:
                $creditCard = new CreditCardPaymentMethod();
                $this->amount = $creditCard->pay($amount);
                break;
            case self::PAYMENT_TYPE_DEBIT_CARD:
                $debitCard = new DebitCardPaymentMethod();
                $this->amount = $debitCard->pay($amount);
                break;
            case self::PAYMENT_TYPE_CASH:
                $cash = new CashPaymentMethod();
                $this->amount = $cash->pay($amount);
                break;
            default:
                throw new \InvalidArgumentException('Invalid payment method');
        }
    }

    public function getAmount(): float
    {
        return $this->amount;
    }
}

Veja a implementação dos testes para essa estratégia sem Strategy:

<?php

declare(strict_types=1);

namespace Growthdev\DesignPatterns\Tests\Behavioral\Strategy\Payment;

use Growthdev\DesignPatterns\Behavioral\Strategy\Payment\PaymentProcessorWithoutStrategy;
use PHPUnit\Framework\TestCase;

final class PaymentProcessorWithoutStrategyTest extends TestCase
{
    public function testCanProcessPaymentWithCashPaymentMethod(): void
    {
        $paymentProcessor = new PaymentProcessorWithoutStrategy(
            PaymentProcessorWithoutStrategy::PAYMENT_TYPE_CASH
        );
        $paymentProcessor->processPayment(100.00);

        $this->assertEquals(90.00, $paymentProcessor->getAmount());
    }

    public function testCanProcessPaymentWithDebitCardPaymentMethod(): void
    {
        $paymentProcessor = new PaymentProcessorWithoutStrategy(
            PaymentProcessorWithoutStrategy::PAYMENT_TYPE_DEBIT_CARD
        );
        $paymentProcessor->processPayment(100.00);

        $this->assertEquals(95.00, $paymentProcessor->getAmount());
    }

    public function testCanProcessPaymentWithCreditCardPaymentMethod(): void
    {
        $paymentProcessor = new PaymentProcessorWithoutStrategy(
            PaymentProcessorWithoutStrategy::PAYMENT_TYPE_CREDIT_CARD
        );
        $paymentProcessor->processPayment(100.00);

        $this->assertEquals(100.00, $paymentProcessor->getAmount());
    }
}

Aparentemente, nas implementações dos testes, são muitos parecidos em cada instância. Porém, caso você queira criar um novo método de pagamento, por exemplo, Paypal, você teria que modificar o método processPayment além de definir mais uma constante. Isso fere o princípio SOLID, como já reiteramos aqui.

Agora, tente implementar os métodos PayPal e Bitcoin e veja na prática a facilidade de uso com Strategy e qual será a dificuldade caso não utilize-o. Deixe aqui seu comentário, isso vai ajudar muito este blog a crescer e continuar fazendo o seu crescimento como programador, alcançar um outro patamar. Até o próximo artigo!

Confiança Sempre!!!

Fontes:

  • Eric T Freeman; Elisabeth Robson; Bert Bates; Kathy Sierra. Head First Design Patterns, O’Reilly Media, Inc 2004.
  • Steven John Metsker. Design Patterns in C#, Addison-Wesley Professional, 2004.
  • Erich Gramma; Richard Helm; Ralph Johnson; John Vlissides. Design Patterns Elements of Reusable Object-Oriented Software, Addison-Wesley Professional, 1994.

Olá! Sou Walmir, engenheiro de software com MBA em Engenharia de Software e o cérebro por trás do GrowthCode e autor do livro "Além do Código". Se você acha que programação é apenas sobre escrever código, prepare-se para expandir seus horizontes. Aqui, nós vamos além do código e exploramos as interseções fascinantes entre tecnologia, negócios, artes e filosofia. Você está em busca de crescimento na carreira? Quer se destacar em um mercado competitivo? Almeja uma vida mais rica em conhecimento e realização? Então você chegou ao lugar certo. No GrowthCode, oferecemos insights profundos, estratégias comprovadas e um toque de sabedoria filosófica para catalisar seu crescimento pessoal e profissional.

Publicado emDesign PatternPadrões de ProjetosPHPProgramação

4 Comentários

Deixe um comentário

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *