Back-End

21 set, 2012

The Strategy Pattern

Publicidade

Recentemente, um amigo me disse sobre como o “strategy pattern” pode ser usado para impor o “Single Responsibility Principle”, ou Princípio da Responsabilidade Única, (SRP) ao utilizar “Tell Don’t Ask”, ou Diga Não Pergunte, (TDA). Pretendo algum dia discutir isso melhor, mas achei que primeiro seria uma boa ideia definir o strategy pattern, usando o exemplo do ShoppingCart:

Primeiro uma definição: em termos mais simples, você pode definir o strategy pattern como dizer a um objeto para fazer um trabalho usando outro objeto.

Para esclarecer isso melhor vou redesenhar o ShoppingCart um pouco, fornecendo um método pay():

public class ShoppingCart {

private final List<Item> items;

public ShoppingCart() {
items = new ArrayList<Item>();
}

public void addItem(Item item) {

items.add(item);
}

public double calcTotalCost() {

double total = 0.0;
for (Item item : items) {
total += item.getPrice();
}

return total;
}

public boolean pay(PaymentMethod method) {

double totalCost = calcTotalCost();
return method.pay(totalCost);
}
}

A primeira coisa para se observar no método pay() é que ele leva um parâmetro de tipo PaymentMethod – o PaymentMethod é o “outro” objeto da minha definição acima.

A próxima coisa a se fazer é definir o PaymentMethod como uma interface. Por que uma interface? É porque o poder dessa técnica é que você pode decidir em tempo de execução qual o tipo concreto vai passar para o ShoppingCart para fazer o pagamento. Por exemplo, dada a interface Payment:

public interface PaymentMethod {

public boolean pay(double amount);

}

você pode então definir qualquer objeto de pagamento concreto, como um Visa ou um MasterCard por exemplo:

public class Visa implements PaymentMethod {

private final String name;
private final String cardNumber;
private final Date expires;

public Visa(String name, String cardNumber, Date expires) {
super();
this.name = name;
this.cardNumber = cardNumber;
this.expires = expires;
}

@Override
public boolean pay(double amount) {

// Open Comms to Visa
// Verify connection
// Paybill using these details
return true; // if payment goes through
}

}

… e

public class MasterCard implements PaymentMethod {

private final String name;
private final String cardNumber;
private final Date expires;

public MasterCard(String name, String cardNumber, Date expires) {
super();
this.name = name;
this.cardNumber = cardNumber;
this.expires = expires;
}

@Override
public boolean pay(double amount) {

// Open Comms to Mastercard
// Verify connection
// Paybill using these details
return true; // if payment goes through
}

}

A última coisa a fazer é demonstrar isso com o teste unitário: payBillUsingVisa

@Test
public void payBillUsingVisa() {

ShoppingCart instance = new ShoppingCart();

Item a = new Item("gloves", 23.43);
instance.addItem(a);

Item b = new Item("hat", 10.99);
instance.addItem(b);

Date expiryDate = getCardExpireyDate();
PaymentMethod visa = new Visa("CaptainDebug", "1234234534564567", expiryDate);

boolean result = instance.pay(visa);
assertTrue(result);

}

private Date getCardExpireyDate() {
Calendar cal = Calendar.getInstance();
cal.clear();
cal.set(2015, Calendar.JANUARY, 21);
return cal.getTime();
}

No código acima, você pode ver que crio um ShoppingCart e depois adiciono alguns itens. Finalmente, crio um novo PaymentMethod na forma de um objeto Visa e coloco na função pay(PaymentMethod method), que é o ponto crucial da questão. Em uma situação diferente, eu poderia facilmente ter criado um objeto MasterCard e usado um substituto direto para o Visa – ou seja, o objeto que é passado como um argumento é determinado em tempo de execução.

E isso define o strategy pattern, mas não é o fim do artigo. Se você já usou Spring mas nunca ouviu falar do strategy pattern, tudo isso deve parecer um pouco familiar. Isso porque os caras do Spring utilizam o Strategy para sustentar toda a sua tecnologia. Se eu pegar o meu exemplo acima e fizer algumas pequenas alterações, posso chegar a:

@Component
public class SpringShoppingCart {

private final List<Item> items;

@Autowired
@Qualifier("Visa")
private PaymentMethod method;

public SpringShoppingCart() {
items = new ArrayList<Item>();
}

public void addItem(Item item) {

items.add(item);
}

public double calcTotalCost() {

double total = 0.0;
for (Item item : items) {
total += item.getPrice();
}

return total;
}

public boolean pay() {

double totalCost = calcTotalCost();
return method.pay(totalCost);
}
}

A única diferença entre essa encarnação e a primeira é que a classe Strategy Visa é injetada pelo Spring a quando for carregada utilizando a anotação @Autowired. Para resumir isso, acho que isso significa que o strategy é o padrão mais popular do mundo.

***

Texto original disponível em http://www.captaindebug.com/2012/03/strategy-pattern.html