Executando verificação de segurança...
1

Design Patterns: Strategy

Strategy Design Pattern

Motivação

O design pattern strategy encapsula um algoritmo em uma classe.

Para implementar defina uma família de algorítmos, encapsule cada um deles e faça com que eles sejam intercambiáveis.

O design pattern strategy permite variar o algoritmo independente do cliente que o utiliza.

Exemplo de Aplicação Prática

Vamos desenvolver uma calculadora que deve realizar quatro operações básicas:

  • Adição
  • Subtração
  • Multiplicação
  • Divisão

classDiagram 
    class Strategy { 
        <<interface>> 
        calculate(a: double, b: double): double 
    } 

    class Context {
        - strategy: Strategy
        + Context(strategy: Strategy)
        + doCalculate(a: double, b: double): double
    }

    class Addition {
        + doCalculate(a: double, b: double): double
    }

    class Subtraction {
        + doCalculate(a: double, b: double): double
    }

    class Multiplication {
        + doCalculate(a: double, b: double): double
    }

    class Divison {
        + doCalculate(a: double, b: double): double
    }

    Context *-- Strategy
    Addition <|-- Strategy
    Subtraction <|-- Strategy

Por tanto a primeira coisa que vamos fazer é criar uma interface que deve representar a estratégia a ser utilizada:

classDiagram
    class Strategy {
        <<interface>>
        calculate(a: double, b: double): double
    }
package br.com.jorgerabellodev.strategy.strategies;

public interface Strategy {
    double calculate(double a, double b);
}

O próximo passo é criar as classes que representam as operações, essas classes devem implementar a interface Strategy:

classDiagram 
    class Strategy { 
        <<interface>> 
        calculate(a: double, b: double): double 
    } 

    class Addition {
        + calculate(a: double, b: double): double 
    }

    class Subtraction {
        + calculate(a: double, b: double): double 
    }

    class Multiplication {
        + calculate(a: double, b: double): double 
    }

    class Divison {
        + calculate(a: double, b: double): double 
    }

    Addition <|-- Strategy
    Subtraction <|-- Strategy
    Multiplication <|-- Strategy
    Divison <|-- Strategy
package br.com.jorgerabellodev.strategy.strategies;

public final class Addition implements Strategy {
    @Override
    public double calculate(double a, double b) {
        System.out.print(a + " + " + b + " = ");
        return a + b;
    }
}

package br.com.jorgerabellodev.strategy.strategies;

public final class Subtraction implements Strategy {
    @Override
    public double calculate(double a, double b) {
        System.out.print(a + " - " + b + " = ");
        return a - b;
    }
}

package br.com.jorgerabellodev.strategy.strategies;

public final class Multiplication implements Strategy {
    @Override
    public double calculate(double a, double b) {
        System.out.print(a + " * " + b + " = ");
        return a * b;
    }
}

package br.com.jorgerabellodev.strategy.strategies;

import br.com.jorgerabellodev.strategy.exceptions.DivisionException;

public final class Division implements Strategy {
    @Override
    public double calculate(double a, double b) {
        if (b <= 0) {
            throw new DivisionException("O divisor deve ser maior que zero !");
        }
        System.out.print(a + " / " + b + " = ");
        return a / b;
    }
}

Para o caso da divisão vamos criar uma exception que deve ser lançada quando for passado um divisor menor ou igual a zero:

classDiagram
    class DivisionException{
        DivisionException(message: String)
    }
    
    class RuntimeException{
    }
    DivisionException <|-- RuntimeException
package br.com.jorgerabellodev.strategy.exceptions;

public class DivisionException extends RuntimeException {
    public DivisionException(String message) {
        super(message);
    }
}

No nosso próximo passo, vamos implementar uma classe que representa um determinado contexto, essa classe em seu construtor deve receber a interface Strategy, assim ao instanciar um contexto, poderemos passar qualquer classe que implemente a interface Strategy, repare que aqui estamos nos valendo do poder do polimorfismo:

classDiagram 
    class Strategy { 
        <<interface>> 
        calculate(a: double, b: double): double 
    } 

    class Context {
        - strategy: Strategy
        + Context(strategy: Strategy)
        + doCalculate(a: double, b: double): double
    }

    Context *-- Strategy
package br.com.jorgerabellodev.strategy.strategies;

public class Context {
    private final Strategy strategy;

    public Context(Strategy strategy) {
        this.strategy = strategy;
    }

    public double doCalculate(double a, double b) {
        return this.strategy.calculate(a, b);
    }
}

Execução e Uso

Feito isso, podemos simplesmente executar as operações da seguinte forma:

package br.com.jorgerabellodev.strategy;

import br.com.jorgerabellodev.strategy.strategies.*;

public class Main {
    public static void main(String[] args) {

        Context additionContext = new Context(new Addition());

        Context subtractionContext = new Context(new Subtraction());

        Context multiplicationContext = new Context(new Multiplication());

        Context divisionContext = new Context(new Division());

        System.out.println(additionContext.doCalculate(10, 2));
        System.out.println(subtractionContext.doCalculate(10, 2));
        System.out.println(multiplicationContext.doCalculate(10, 2));
        System.out.println(divisionContext.doCalculate(10, 2));
    }
}

Testes Unitários

Agora vamos implementar alguns testes unitários, demonstrando o funcionamento da implementação:

package br.com.jorgerabellodev.strategy.strategies;

import br.com.jorgerabellodev.strategy.exceptions.DivisionException;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertThrows;

class ContextTest {

    @Test
    void givenAnAdditionStrategyShouldReturnTheAdditionResult() {
        Context additionContext = new Context(new Addition());

        double result = additionContext.doCalculate(10, 2);

        Assertions.assertThat(result).isEqualTo(12);
    }

    @Test
    void givenASubtractionStrategyShouldReturnTheSubtractionResult() {
        Context subtractionContext = new Context(new Subtraction());

        double result = subtractionContext.doCalculate(10, 2);

        Assertions.assertThat(result).isEqualTo(8);
    }

    @Test
    void givenAMultiplicationStrategyShouldReturnTheMultiplicationResult() {
        Context multiplicationContext = new Context(new Multiplication());

        double result = multiplicationContext.doCalculate(10, 2);

        Assertions.assertThat(result).isEqualTo(20);
    }

    @Test
    void givenADivisionStrategyShouldReturnTheDivisionResult() {
        Context divisionContext = new Context(new Division());

        double result = divisionContext.doCalculate(10, 2);

        Assertions.assertThat(result).isEqualTo(5);
    }

    @Test
    void givenADivisionStrategyUsingAZeroDivisorShouldThrowDivisionException() {
        Context divisionContext = new Context(new Division());

        DivisionException divisionException = assertThrows(DivisionException.class, () -> divisionContext.doCalculate(10, 0));

        Assertions.assertThat(divisionException.getMessage()).isEqualTo("O divisor deve ser maior que zero !");
    }
}

Caso de Uso

Vamos imaginar que você esteja desenvolvendo parte de um software de e-commerce, e que seja possível pagar as compras realizadas com tipos de pagamentos diferentes, sendo eles:

  • MasterCard
  • VisaCard
  • PayPall

classDiagram 
    class Payment { 
        <<interface>> 
        pay(price: double): String
    } 

    class PaymentMethod {
        - payment: Payment
        + PaymentManager(payment: Payment)
        + doPayment(price: double): String
    }

    class MasterCard {
        + pay(price: double): String
    }

    class VisaCard {
        + pay(price: double): String
    }

    class PayPall {
        + pay(price: double): String
    }

    PaymentMethod *-- Payment
    MasterCard <|-- Payment
    VisaCard <|-- Payment
    PayPall <|-- Payment

Primeiramente vamos criar a interface que representa um pagamento:

classDiagram 
    class Payment { 
        <<interface>> 
        pay(price: double): String
    } 

package br.com.jorgerabellodev.strategy.strategies;

public interface Payment {
    String pay(double price);
}

Na sequência, vamos criar uma classe chamada PaymentMethod essa classe será responsável por aplicar as regras de pagamento, conforme o contexto:

classDiagram 
    class Payment { 
        <<interface>> 
        pay(price: double): String
    } 

    class PaymentMethod {
        - payment: Payment
        + PaymentManager(payment: Payment)
        + doPayment(price: double): String
    }
    PaymentMethod *-- Payment
package br.com.jorgerabellodev.strategy.strategies;

public class PaymentMethod {
    private final Payment payment;

    public PaymentMethod(Payment payment) {
        this.payment = payment;
    }

    public String doPay(double price) {
        return payment.pay(price);
    }
}


Nosso próximo passo é criar as classes que representam os pagamentos, lembre-se que essas classes devem implementar a interface Payment:

classDiagram 
    class Payment { 
        <<interface>> 
        pay(price: double): String
    } 

    class MasterCard {
        + pay(price: double): String
    }

    class VisaCard {
        + pay(price: double): String
    }

    class PayPall {
        + pay(price: double): String
    }

    MasterCard <|-- Payment
    VisaCard <|-- Payment
    PayPall <|-- Payment
package br.com.jorgerabellodev.strategy.strategies;

public class MasterCard implements Payment {
    @Override
    public String pay(double price) {
        return "Pay " + price + " using MasterCard";
    }
}


package br.com.jorgerabellodev.strategy.strategies;

public class VisaCard implements Payment {
    @Override
    public String pay(double price) {
        return "Pay " + price + " using VisaCard";
    }
}

package br.com.jorgerabellodev.strategy.strategies;

public class PayPall implements Payment {
    @Override
    public String pay(double price) {
        return "Pay " + price + " using PayPall";
    }
}

Agora podemos executar, utilizando a implementação:

package br.com.jorgerabellodev.strategy;

import br.com.jorgerabellodev.strategy.strategies.MasterCard;
import br.com.jorgerabellodev.strategy.strategies.PayPall;
import br.com.jorgerabellodev.strategy.strategies.PaymentMethod;
import br.com.jorgerabellodev.strategy.strategies.VisaCard;

import java.util.Scanner;

public class Main {
    public static void main(String[] args) {
        Scanner teclado = new Scanner(System.in);

        System.out.println("You need tp pay $25 for mobile phone");

        System.out.println("Please select payment method");
        System.out.println("1: MasterCard");
        System.out.println("2: VisaCard");
        System.out.println("3: PayPall");

        int option = teclado.nextInt();

        PaymentMethod paymentManager = null;

        switch (option) {
            case 1 -> paymentManager = new PaymentMethod(new MasterCard());
            case 2 -> paymentManager = new PaymentMethod(new VisaCard());
            case 3 -> paymentManager = new PaymentMethod(new PayPall());
            default -> System.err.println("ERROR: You need to select a valid payment method");
        }

        System.out.println(paymentManager.doPay(25));
    }
}

Como de costume vamos implementar os testes:

package br.com.jorgerabellodev.strategy.strategies;

import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;

@TestMethodOrder(MethodOrderer.DisplayName.class)
class PaymentMethodTest {

    @Test
    @DisplayName("Given a master card payment method should return \"Pay 35.0 using MasterCard\" Message")
    void givenAMasterCardPaymentMethodShouldReturnPay35UsingMasterCardMessage() {

        PaymentMethod masterCardPaymentMethod = new PaymentMethod(new MasterCard());

        String message = masterCardPaymentMethod.doPay(35);

        Assertions.assertThat(message).isEqualTo("Pay 35.0 using MasterCard");
    }

    @Test
    @DisplayName("Given a visa card payment method should return \"Pay 35.0 using VisaCard\" Message")
    void givenAVisaCardPaymentMethodShouldReturnPay35UsingVisaCardMessage() {

        PaymentMethod visaCardPaymentMethod = new PaymentMethod(new VisaCard());

        String message = visaCardPaymentMethod.doPay(35);

        Assertions.assertThat(message).isEqualTo("Pay 35.0 using VisaCard");
    }

    @Test
    @DisplayName("Given a pay pall payment method should return \"Pay 35.0 using PayPall\" Message")
    void givenAVisaCardPaymentMethodShouldReturnPay35UsingPayPallMessage() {

        PaymentMethod payPallPaymentMethod = new PaymentMethod(new PayPall());

        String message = payPallPaymentMethod.doPay(35);

        Assertions.assertThat(message).isEqualTo("Pay 35.0 using PayPall");
    }
}

Código

Código No BitBucket

Carregando publicação patrocinada...