Ir ao conteúdo

Padrão de Projeto Strategy em PHP com exemplo

Atualizado pela última vez em 16 de novembro de 2021

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.
Publicado emDesign PatternPadrões de ProjetosPHPProgramação

2 Comentários

  1. Valdir

    Muito massa , parabéns pelo artigo e obrigado por compartilhar!

    • Opa! Muito obrigado Valdir!

      Fico feliz que tenha gostado!

      Confiança Sempre!!!

Deixe um comentário

O seu endereço de e-mail não será publicado.