quarta-feira, 18 de junho de 2014

Design Pattern - Strategy

Motivação

Para se fazer um bom uso de orientação a objetos é necessários que se respeite alguns princípios. Um desses princípios é o open close ou, no bom português, aberto fechado.

Mas o que significa esse tal princípio aberto fechado?

A grosso modo, esse princípio diz que devemos programar de forma que nosso código fique aberto para extensão e fechado para modificação.

Ok, acho que entendi. Mas qual é a relação entre o padrão strategy e o princípio open close?

Para podermos visualizar a ligação entre o strategy e o princípio open close vamos desenvolver um exemplo sem o uso do strategy. O vínculo entre o princípio open close e o padrão strategy ficará claro quando fizermos a refatoração do código com o uso do padrão strategy.

Explanação

Imagine que você está programando um sistema de vendas. Já está quase tudo pronto, só faltando programar a forma de pagamento.

Como o cliente, a princípio, só vai fazer vendas à vista a tarefa parece simples. Os trechos de código (em java) a seguir resolvem o problema.

public class Sales {
  private List dataBaseFake = new ArrayList();

  public void payment(float total) {
    Instalment instalment = new Instalment(total, DateTime.now());
    this.dataBaseFake.addAll(instalment);
  }
}
public class Main {
  public static void main(String[] args) {
    Sales sale = new Sales();

    sale.payment(1000.00f);
  }
}

Legal, problema resolvido, mas agora o cliente quer a opção de fazer o pagamento em três vezes sem juros.

Podemos abrir o método payment e inserir um if para avaliar o novo requisito.

Isso funciona? Como visto no código abaixo, vemos que funciona. Mas perceba que para fazer isso tivemos que abrir o método e fazer uma mudança. Essa abertura para mudança vai contra o princípio open close.

public class Sales {
  private List dataBaseFake = new ArrayList();
  private DateTime today = DateTime.now();

  public void payment(float total, String condition) {
    if (condition.equals("Avista")) {
      Instalment instalment = new Instalment(total, today);
      this.dataBaseFake.addAll(instalment);
    } else if (condition.equals("DuasVezesSemJuros")) {
      for (int i = 0; i < 3; i++) {
        Instalment instalment =
          new Instalment(total/3, today.plusMonths(i));
        instalments.add(instalment);
      }
      this.dataBaseFake.addAll(instalments);
    }
  }
}
public class Main {
  public static void main(String[] args) {
    Sales sale = new Sales();
    sale.payment(1000.00f, "DuasVezesSemJuros");
  }
}

Há a possibilidade do cliente pedir para implementar mais um tipo de condição de pagamento?

Eu diria que a probalidade de isto acontecer é de 110%.

E aí, para cada pedido de mundança, abrimos, mudamos e refazemos todos os testes e de quebra deixamos o nosso código mais confuso? Não, né? É aí que entra o padrão strategy.

Como percebemos, no problema que estamos resolvendo, a quantidade de estratégias de pagamento aumentou e tende a aumentar ainda mais (5 vezes sem juros, 12 vezes com juros, 48 vezes com JUROS, ...).

Para resolver o problema com o padrão Strategy vamos criar a Interface PaymentStrategy e vamos definir para esta Interface o método calculatedInstalments.

public interface PaymentStrategy {
  public List calculatedInstalments(float total);
}

Agora reescrevemos a classe Sales como a seguir:

public class Sales {
  private List dataBaseFake = new ArrayList();

  public void payment(float total, PaymentStrategy ps) {
    this.dataBaseFake.addAll(ps.calculatedInstalments(total));
  }
}

Agora precisamos escrever o código das condições de pagamento solicitadas pelo cliente. Porém, para que haja compatibilidade entre as implementações de cada forma de pagamento, todas as implementações devem ter a mesma cara, isto é, todas devem implementar a mesma Interface, a Interface PaymentStrategy. Veja a implementação da condição de pagamento Avista.

public class Avista implements PaymentStrategy {

  @Override
  public List calculatedInstalments(float total) {
    List instalments = new ArrayList();

    Instalment instalment = new Instalment(total, DateTime.now());

    instalments.add(instalment);
    return instalments;
  }
}

Agora veja a implementação da condição de pagamento TresVezesSemJuros.

public class TresVezesSemJuros implements PaymentStrategy {

  final static int MAX_INSTALMENTS = 3;

  @Override
  public List calculatedInstalments(float total) {
    List instalments = new ArrayList();
    DateTime today = DateTime.now();

    for (int i = 0; i < MAX_INSTALMENTS; i++) {
        Instalment instalment =
                new Instalment(total/MAX_INSTALMENTS, today.plusMonths(i));
        instalments.add(instalment);
    }
    return instalments;
  }
}

Agora, um exemplo de uso:

public class Main {
  public static void main(String[] args) {
    Sales sale = new Sales();

    sale.payment(1000.00f, new Avista());

    Sales sale2 = new Sales();

    sale2.payment(1000.00f, new TresVezesSemJuros());

  }
}

Não importa quantos métodos de pagamento surgirem de agora em diante, a classe Sales não precisa mais ser aberta para mudanças, isto é, está fechada para mudanças. Mas note que, ao mesmo tempo, a classe está aberta para extensão, isto é, pode-se aumentar o número de formas de pagamento indefinidamente.

Conclusão

Gostaria que ficasse claro que o padrão de projeto aqui apresentado não é de como se resolver o problema de quantidade de prestações que uma loja oferece. O padrão que deve ser observado é de um problema que pode ser resolvido com várias estratégias diferentes.

Um exemplo de uso de estratégias é o de um jogo do tipo Counter Strike, onde um mesmo jogador pode usar várias estratégias, isto é, pode em dado momento usar uma faca, em outra circunstância pode usar uma metralhadora ou outros tipos de armas conforme demanda a situação.

Imagine que a comunidade que joga esse jogo nos moldes do Counter Strike quer como arma o martelo do Thor. Para a equipe de desenvolvimento do jogo, seria muito arriscado abrir um código que já funciona perfeitamente, enfiar um if lá no meio e implementar o martelo do Thor. Mas se a equipe tem a possibilidade de apenas extender o jogo, isto é, apenas implementar o martelo do Thor sem precisar mexer no que já funciona, isso é bem mais eficiente e seguro.