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));Card: €80.00Cash on delivery: €82.50Voucher: €70.00Bank transfer: €78.40Installments: €84.00It 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(IPaymentMethodin 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);}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); }}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));Card: €80.00Cash on delivery: €82.50Voucher: €70.00Checkout 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
- The checkout gets a card: its method field points to the object Card.
- total(80): the checkout doesn't know the method. It just asks the strategy for the sum and gets 80.00 back.
- A new method. setMethod points the field to the object CashOnDelivery. Not a single line of Checkout changes.
- The same total(80), a different answer: 82.50.
- A voucher for 10: an object with its own field value. Nothing points to the old strategies any more.
- 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));Bank transfer: €78.40Installments: €84.00Not 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: Installments is not abstract and does not override abstract method total(double) in PaymentMethodWith 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));}cash on deliverycash on delivery: €82.50An unknown method never reaches the checkout:
PayPalNo such payment method: PayPalThe 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));Cash on delivery: €82.50A 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));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));Order preview: €70.00Payment: €80.00The preview showed €70, and the payment asked for €80. Which line is the bug on?
Pick the line with the bug.
The checkout calls the strategy every time it needs the sum: for the preview, for the invoice, for the payment. The strategy should only compute. Once it changes itself, the second question gets a different answer. Remove the line: the sum is €70 both times. Whether the voucher has been used is recorded in the order, after the payment goes through.
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.
Apple Pay: €80.80More 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
Related lessons
Section titled "Related lessons"- Builds on: Throwing and your own exceptions · HashMap and HashSet · Lambdas · Design patterns
- Continues in: Observer · Factory Method and Simple Factory · Singleton and Dependency Injection · Adapter and Facade
- Tasks on this topic: Design patterns