Skip to content
wannacode

Strategy

In short: Strategy puts each rule (such as a payment method) in its own object behind a shared interface, so you add new rules without touching the code that uses them.

Problem

The checkout that knows every payment method

The online shop takes five payment methods. Cash on delivery costs €2.50 more, a voucher takes €10 off, a bank transfer gets a 2% discount, and paying in installments costs 5% more. The checkout computes the sum like this:

static double total(String method, double amount) {
if (method.equals("card")) {
return amount;
} else if (method.equals("cash on delivery")) {
return amount + 2.50;
} else if (method.equals("voucher")) {
return Math.max(amount - 10, 0);
} else if (method.equals("bank transfer")) {
return amount * 0.98;
} else if (method.equals("installments")) {
return amount * 1.05;
}
throw new IllegalArgumentException("Unknown payment method: " + method);
}
System.out.printf("Card: €%.2f%n", total("card", 80));
System.out.printf("Cash on delivery: €%.2f%n", total("cash on delivery", 80));
System.out.printf("Voucher: €%.2f%n", total("voucher", 80));
System.out.printf("Bank transfer: €%.2f%n", total("bank transfer", 80));
System.out.printf("Installments: €%.2f%n", total("installments", 80));
Output
Card: €80.00
Cash on delivery: €82.50
Voucher: €70.00
Bank transfer: €78.40
Installments: €84.00

It works. But every month a new method arrives, and each one is a new branch in this method. The same chain is in the invoice and in the confirmation email too. Someone adds Apple Pay in two of the three places, and the customer gets an email saying "Unknown payment method".

Vouchers aren't only for €10 any more. To pass the value in, the method gets a third parameter, and every place that calls it changes, even for card payments, where there is no voucher. The code that every payment goes through is touched with every change.

The idea

Strategy: the rule on its own, the checkout asks

What changes is one thing: how the sum is computed. What doesn't change is the checkout: it takes the sum, asks and rounds.

So the rule moves into a separate object. All methods share one common shape: an interface with one method that takes a sum and returns how much to pay. The checkout keeps a reference to such an object and calls the method without knowing which object it is.

This is the Strategy pattern. It has three roles:

  • the checkout keeps the strategy and calls it. Books call this the context;
  • the common shape: the interface PaymentMethod (IPaymentMethod in C#);
  • the concrete strategies: one class for each method.

A new method is a new strategy. The checkout isn't touched.

See it

The checkout and three methods

The common shape:

interface PaymentMethod {
double total(double amount);
}
Three methods, each in its own class. The voucher has a field with its value, so the value comes in the constructor instead of being a parameter for everyone:
class Card implements PaymentMethod {
@Override
public double total(double amount) {
return amount;
}
}
class CashOnDelivery implements PaymentMethod {
@Override
public double total(double amount) {
return amount + 2.50;
}
}
class Voucher implements PaymentMethod {
private double value;
public Voucher(double value) {
this.value = value;
}
@Override
public double total(double amount) {
return Math.max(amount - value, 0);
}
}
The checkout keeps the strategy in a field and computes with it. setMethod changes it:
class Checkout {
private PaymentMethod method;
public Checkout(PaymentMethod method) {
this.method = method;
}
public void setMethod(PaymentMethod method) {
this.method = method;
}
public double total(double amount) {
return Math.round(method.total(amount) * 100) / 100.0;
}
}
Checkout checkout = new Checkout(new Card());
System.out.printf("Card: €%.2f%n", checkout.total(80));
checkout.setMethod(new CashOnDelivery());
System.out.printf("Cash on delivery: €%.2f%n", checkout.total(80));
checkout.setMethod(new Voucher(10));
System.out.printf("Voucher: €%.2f%n", checkout.total(80));
Output
Card: €80.00
Cash on delivery: €82.50
Voucher: €70.00

Checkout checkout = new Checkout(new Card());
System.out.printf("Card: €%.2f%n", checkout.total(80));
checkout.setMethod(new CashOnDelivery());
System.out.printf("Cash on delivery: €%.2f%n", checkout.total(80));
checkout.setMethod(new Voucher(10));
System.out.printf("Voucher: €%.2f%n", checkout.total(80));
Step by step, in words
  1. The checkout gets a card: its method field points to the object Card.
  2. total(80): the checkout doesn't know the method. It just asks the strategy for the sum and gets 80.00 back.
  3. A new method. setMethod points the field to the object CashOnDelivery. Not a single line of Checkout changes.
  4. The same total(80), a different answer: 82.50.
  5. A voucher for 10: an object with its own field value. Nothing points to the old strategies any more.
  6. The voucher takes 10 off: 70.00. Three methods, and the checkout is the same object with the same code the whole time.

The checkout is one object with one piece of code for the whole program. Only what the field points to changes.

See it

A new method without touching the checkout

Bank transfer and installments come as two new classes:

class BankTransfer implements PaymentMethod {
@Override
public double total(double amount) {
return amount * 0.98;
}
}
class Installments implements PaymentMethod {
@Override
public double total(double amount) {
return amount * 1.05;
}
}
Checkout checkout = new Checkout(new BankTransfer());
System.out.printf("Bank transfer: €%.2f%n", checkout.total(80));
checkout.setMethod(new Installments());
System.out.printf("Installments: €%.2f%n", checkout.total(80));
Output
Bank transfer: €78.40
Installments: €84.00

Not a single line of the checkout changed. You can't break card payments while adding transfers, because the card code isn't in front of you.

There is one more thing. If the new class forgets the method or misspells its name, the program doesn't compile:

class Installments implements PaymentMethod {
public double totl(double amount) {
return amount * 1.05;
}
}
Error
error: Installments is not abstract and does not override abstract method total(double) in PaymentMethod

With the chain of ifs, a misspelt method name only shows up when a customer chooses it.

See it

Which method the customer chooses

The customer types the method as text. The chain of ifs that picked the rule becomes a table: name → strategy.

Map<String, PaymentMethod> methods = Map.of(
"card", new Card(),
"cash on delivery", new CashOnDelivery(),
"voucher", new Voucher(10));
Scanner scanner = new Scanner(System.in);
String choice = scanner.nextLine();
PaymentMethod method = methods.get(choice);
if (method == null) {
System.out.println("No such payment method: " + choice);
} else {
Checkout checkout = new Checkout(method);
System.out.printf("%s: €%.2f%n", choice, checkout.total(80));
}
Input
cash on delivery
Output
cash on delivery: €82.50

An unknown method never reaches the checkout:

Input
PayPal
Output
No such payment method: PayPal

The choice is still in one place, but now it is data. A new method is a new row in the table and a new class.

More precisely, if you are curious

PaymentMethod has one method, so it is a functional interface, and a strategy can also be a lambda:

Checkout checkout = new Checkout(amount -> amount + 2.50);
System.out.printf("Cash on delivery: €%.2f%n", checkout.total(80));
Output
Cash on delivery: €82.50

A lambda is enough for a one-line rule. A class pays off when the strategy has its own field (the voucher), a name or more than one method.

Try it

Predict the output

The order is €8, and the voucher is €10. Then the checkout switches to cash on delivery. What does the program print?

Checkout checkout = new Checkout(new Voucher(10));
System.out.printf("€%.2f%n", checkout.total(8));
checkout.setMethod(new CashOnDelivery());
System.out.printf("€%.2f%n", checkout.total(8));
Answer

Try it

Find the bug

A voucher should be used once. Someone "uses it up" right inside the strategy:

class Voucher implements PaymentMethod {
private double value;
public Voucher(double value) {
this.value = value;
}
@Override
public double total(double amount) {
double result = Math.max(amount - value, 0);
value = 0;
return result;
}
}
Checkout checkout = new Checkout(new Voucher(10));
System.out.printf("Order preview: €%.2f%n", checkout.total(80));
System.out.printf("Payment: €%.2f%n", checkout.total(80));
Output
Order preview: €70.00
Payment: €80.00

The preview showed €70, and the payment asked for €80. Which line is the bug on?

Pick the line with the bug.

The idea

When you don't need it

Strategy has a cost: an interface and a class for every variant, instead of one if. It isn't worth it when:

  • there are two variants and they don't change. "With VAT or without" is one if;
  • the rule is used in one place and nobody touches it;
  • there is only one strategy, "just in case". An interface with one implementation is code you have to read without getting anything for it.

It is worth it when the variants grow, change while the program runs (the customer chooses), or different parts of the program need different rules.

Exercise

Apple Pay without touching the checkout

Solve the task in your editor.

Apple Pay. The shop adds Apple Pay with a 1% fee on the sum. Add a class ApplePay and pay an €80 order with it. The checkout and the other methods are not touched.

Output
Apple Pay: €80.80

More tasks

  • Free cash on delivery over €100. Write a cash-on-delivery strategy that doesn't charge €2.50 for orders over €100. Run it with €80 and with €120.
  • The method from the input. Add bank transfer and installments to the table from the card "Which method the customer chooses". Read the amount from the input too.
  • A gift card with a balance. The card has a balance of €50: it pays what it can, and the rest is paid by card. Think about where the new balance is recorded. Does the strategy change it, or the order after the payment?

Test

Check yourself

1. The shop adds Apple Pay. What do you write with Strategy?
2. What does the checkout keep with Strategy?
3. The voucher strategy sets its value to zero every time total is called. The checkout asks once for the preview and once for the payment. What happens?
4. When don't you need Strategy?

Made by Ivo Gergov as part of Evo Tech's campaign for accessible programming education ·