Kill the virus with a strategy pattern! Java Strategy Pattern with practical examples


13 Mar 2020  Michal Fabjanski  19 mins read.

Strategy Design Pattern in Java

Todat I’ll take you to my lab. I’ll show you how to make your code cleaner. Any project can be created by multiple programmers at the same time, and each of them makes more bricks - being forced to understand other people’s bricks.
Because you are in my lab, we will start working on real examples - starting with eccomerce, ending with virus treatment!

Strategy Pattern introduction

The strategy pattern next to the factory pattern is one of the most frequently used design patterns. It is easy to understand and to implement. It is one of the behavioral patterns, i.e. those that describe certain behavior.
Strategy defines a family of interchangeable algorithms (in the form of classes) that are used to solve the same problem in several different ways.

Implementation

strateg-pattern-diagram
The Context class contains a reference to the StrategyInterface strategy object. The strategy object can be injected by the constructor or setter. The Context class method uses the strategy object to finalize the operation. Strategy classes Strategy1, Strategy2, etc. implement StrategyInterface interface methods. Each of these implementations of StrategyInterface solves a similar problem in a slightly different way. The above diagram can be implemented in the following way:

      
public class Context {

    private Strategy strategy;

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

    public void setStrategy(Strategy strategy) {
        this.strategy = strategy;
    }

    public void run(Object args) {
        strategy.action(args);
    }
}  

// -----------------------------------------------------------  
public class Strategy1 implements Strategy {  
  
 @Override 
 public void action(Object args) { //do action specific for Strategy1 
 }}  
  
// -----------------------------------------------------------   
public class Strategy2 implements Strategy {  
  
 @Override 
 public void action(Object args) { //do action specific for Strategy2 
 }}  
// -----------------------------------------------------------   
interface Strategy {  
  
 void action(Object args);}  

Before calling the target method, the client selects the injected strategy depending on the condition:

  
Context context = new Context(new Strategy1());  
String arg = "args passed to strategy 1";  
context.run(arg);  
  
//new conditions 
context.setStrategy(new Strategy2());  
arg = "args for strategy 2";  
context.run(arg);  

Strategy Pattern Real Life Examples

Let’s get to the practical part. I will show you some examples where Strategy pattern is a perfect solution.

E-commerce - international shipping system with the strategy pattern

Imagine that we own an online shop with many different products. At the beginning, we send products only to the USA, but we want to enter a new market - Europe. We also plan to enter the Australian and African markets.
By following the Open/Closed Principle we do not want to add more IFs to our class. In this case, the ideal choice is the strategy pattern!

Let’s start from the Shopping.java interface - responsible for price calculation and currency information.

      
public interface Shopping {  
  
 double calculatePrice(List<Product> products); 
 Currency getCurrency();
 }  

Now it’s time for some boring model classes:
Products.java with our products:

      
public class Product {  
  
 private int id; 
 private String name; 
 private double price; 
 private Size size;  
 
 public Product(int id, String name, double price, Size size) { 
 this.id = id; 
 this.name = name; 
 this.price = price; 
 this.size = size; 
 } // getters and setters
 

Cart.java class:

      
public class Cart {

    private Shopping shopping;
    private List<Product> products;

    public Cart(Shopping shopping) {
        this.shopping = shopping;
        products = new ArrayList();
    }

    public double getTotalPrice() {
        return shopping.calculatePrice(products);
    }

    public void addProduct(Product product) {
        products.add(product);
    }
}

Size.java - The price of the products will depend on the size:

      
public enum Size {

    S(1),
    L(2),
    XL(3);

    private int size;

    Size(int size) {
        this.size = size;
    }

    @Override
    public String toString() {
        switch(size) {
            case 1:
                return "S";
            case 2:
                return "L";
            case 3:
                return "XL";
            default:
                return "CUSTOM";
        }
    }
}

Let’s get back to the concrete! It’s time to implement our strategies. Depending on the country of shipping, the relevant customs duty is charged and the shipping cost of EuropeShopping, AmericaShopping increases. Large products (XL-size) have an additional charge.

      
public class AmericaShopping implements Shopping {

    private final double USD_TAX = 0.75;
    private final double DUTY_TAX = 1.0;
    private double DELIVERY_COST = 15;

    @Override
    public double calculatePrice(List<Product> products) {
        double totalPrice = 0;
        for(Product product : products) {
            if(product.getSize() == Size.XL) {
                totalPrice += product.getPrice() * DUTY_TAX;
                DELIVERY_COST += 10;
            }
            else {
                totalPrice += product.getPrice() * DUTY_TAX;
                DELIVERY_COST += 5;
            }
        }
        return (totalPrice + DELIVERY_COST) * USD_TAX;
    }

    @Override
    public Currency getCurrency() {
        return Currency.USD;
    }
}
  
public class EuropeShopping implements Shopping {

    private final double DUTY_TAX = 1.5;
    private double DELIVERY_COST = 11;

    @Override
    public double calculatePrice(List<Product> products) {
        double totalPrice = 0;
        for(Product product : products) {
            totalPrice += product.getPrice() * DUTY_TAX;
            if(product.getSize() == Size.XL)
                DELIVERY_COST += 10;
        }
        return totalPrice;
    }

    @Override
    public Currency getCurrency() {
        return Currency.EUR;
    }
}

The above listing shows the calculation of the final price depending on the location of the shipment and product size.
Example of use:

      
public class Main {

    public static void main(String[] args) {
        Shopping shopping;
//       Define a region for the USA.
//       This is for demo purposes only - it should be injected depending on user address.
        Region region = Region.EUR;

//      Choice of strategy according to user region
        if(region == Region.USA) {
            shopping = new AmericaShopping();
        }
        else {
            shopping = new EuropeShopping();
        }

        Cart cart = new Cart(shopping);
        cart.addProduct(new Product(0, "Dell", 125, Size.L));
        cart.addProduct(new Product(1, "Iphone", 1235, Size.S));
        cart.addProduct(new Product(2, "TV", 535, Size.XL));

        double totalPrice = cart.getTotalPrice();
        System.out.println("Total price : " + totalPrice + " " + shopping.getCurrency());
//        for region = REGION.USA the result is: Total price : 1447.5 USD
//        for region = REGION.EUR the result is: Total price : 2842.5 EUR
    }
}

Number converter with the strategy pattern

Imagine that you want to create a system to convert numbers into different Numeral Systems .
As in the previous example, we will start with the interface:

      
public interface ConvertingStrategy {  
    String convert(int number);  
}  

Time for implementation. We will add support to convert number to octal, binary and hex system:

      
public class BinaryConverter implements ConvertingStrategy {  
    public String convert(int number) {  
        return Integer.toBinaryString(number);  
    }  
}  
  
public class HexaConverter implements ConvertingStrategy {  
    public String convert(int number) {  
        return Integer.toHexString(number);  
    }  
}  
  
public class OctaConverter implements ConvertingStrategy {  
    public String convert(int number) {  
        return Integer.toOctalString(number);  
    }  
}  

According to the diagram at the beginning of the post - we will create a class Context that will use our strategies:

      
public class Context {
    private ConvertingStrategy strategy;

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

    public String convert(int number) {
        return strategy.convert(number);
    }
}

An example of how to use a strategy to convert:

      
public class Main {  
    public static void main(String[] args) {  
        Context ctx = new Context(new HexaConverter());  
        System.out.println(ctx.convert(1000));  
//      Result: 3e8  
//      If you change HexaConverter to BinaryConverter, the result will be: 1111101000  
    }  
}  

Cure the Coronavirus with strategy pattern!

It’s time for something popular :) We will create a strategy to treat viruses. It may seem to you that this is not a very practical use of Strategy Pattern, but imagine a similar situation in any game.

In our case, this could be part of a game taking place in a Hospital For Infectious Diseases!
Very often the strategy is used in games to handle movement. We want a player to either walk or run when he moves, but maybe in the future, he should also be able to swim, fly, teleport, burrow underground, etc.
Let’s return to the hospital game. Just like before, we start with the interface:

      
public interface VaccinationStrategy {  
    String vaccineInjection();  
}  

And some implementations:

      
public class Covid19Treatment implements VaccinationStrategy {  
    public String vaccineInjection() {  
        return "The patient have been cured of infection with Coronavirus";  
    }  
}  
  
public class EbovTreatment implements VaccinationStrategy {  
    public String vaccineInjection() {  
        return "The patient have been cured of infection with Ebov";  
    }  
}  
  
public class SarsTreatment implements VaccinationStrategy {  
    public String vaccineInjection() {  
        return "The patient have been cured of infection with SARS";  
    }  
}  

Now let’s create a class that will work as the Context from the previous point. Let’s name it Treatment.java:

      
public class Treatment {
    private VaccinationStrategy vaccinationStrategy;
    private String patientName;

    public Treatment(String patientName) {
        this.patientName = patientName;
    }

    public void setVaccination(VaccinationStrategy vaccination) {
        this.vaccinationStrategy = vaccination;
    }

    public void treatment() {
        System.out.println("The treatment of the patient: " + this.patientName + " has started");
        System.out.println(vaccinationStrategy.vaccineInjection());
        System.out.println("The patient left the hospital");
        System.out.println("--------------------------------------------------------------------");
    }
}

In the above example, we set the strategy through the setter (instead of the constructor).
Example of use:

   
public class Main {

    public static void main(String[] args) {
        Treatment patient1Treatment = new Treatment("John Duke");
        Treatment patient2Treatment = new Treatment("Elisa Muratti");
        Treatment patient3Treatment = new Treatment("Jeff People");

        patient1Treatment.setVaccination(new Covid19Treatment());
        patient2Treatment.setVaccination(new EbovTreatment());
        patient3Treatment.setVaccination(new SarsTreatment());

        patient1Treatment.treatment();
        patient2Treatment.treatment();
        patient3Treatment.treatment();

        System.out.println("All patients cured. The world is saved!");

    }

//    RESULT:
//The treatment of the patient: John Duke has started
//    The patient have been cured of infection with Coronavirus
//    The patient left the hospital
//--------------------------------------------------------------------
//    The treatment of the patient: Elisa Muratti has started
//    The patient have been cured of infection with Ebov
//    The patient left the hospital
//--------------------------------------------------------------------
//    The treatment of the patient: Jeff People has started
//    The patient have been cured of infection with SARS
//    The patient left the hospital
//--------------------------------------------------------------------
//    All patients cured. The world is saved!

}

Tax calculation system with the Strategy Pattern

We have a system for creating invoices, but we have customers from different tax zones. What do you think will be the right design pattern? Exactly, the strategy!
This time our interface will have one method of calculate():

      
public interface Tax {  
    BigDecimal calculate(Invoice invoice);  
}  
  
// IMPLEMENTATIONS:  
public class DutyTax implements Tax {  
    public BigDecimal calculate(Invoice invoice) {  
        return invoice.getCost()  
                .multiply(BigDecimal.valueOf(0.18));  
    }  
}  
  
public class FederalTax implements Tax {  
    public BigDecimal calculate(Invoice invoice) {  
        return invoice.getCost()  
                .multiply(BigDecimal.valueOf(0.07));  
    }  
}  
  
public class VatTax implements Tax {  
    public BigDecimal calculate(Invoice invoice) {  
        return invoice.getCost()  
                .multiply(BigDecimal.valueOf(0.10));  
    }  
}  

We’ll need the class responsible for the invoice:

      
public class Invoice {
    private BigDecimal cost;

    public Invoice(BigDecimal cost) {
        this.cost = cost;
    }

    public BigDecimal getCost() {
        return cost;
    }

    public void setCost(BigDecimal cost) {
        this.cost = cost;
    }
}

And, as before, the Context class:

      
  
public class Context {  
    private Tax tax;  
  
 public Context(Tax tax) { this.tax = tax; }  
 public BigDecimal calculateTax(Invoice invoice) { return tax.calculate(invoice); }}  

Example of use:

      
public class Main {

    public static void main(String[] args) {

        Invoice invoice = new Invoice(new BigDecimal("1500"));

        Context federal = new Context(new FederalTax());
        Context vat = new Context(new VatTax());
        Context duty = new Context(new DutyTax());

        System.out.println(federal.calculateTax(invoice));
        System.out.println(vat.calculateTax(invoice));
        System.out.println(duty.calculateTax(invoice));

//        Result:
//        105.00
//        150.0
//        270.00
    }
}

There are a lot of examples to come up with. The most common situations in which the strategy pattern is used are:

  • Validation: You need to check items according to some rule, but it is not yet clear what that rule will be, and there are likely to be many of them.
  • Storing information: You want the application to store information to the Database, but later it may be neet to be able to save a file.
  • Games: as I wrote in previous point - strategy is often used in games, e.g. to correctly handle the movement of objects in games.
  • Sorting: You want to sort elements, but you do not know what sorting algorithm should you use (BrickSort, QuickSort or other). This is a common case used in examples of Strategy implementation, so I omitted implementation in this article.
  • Outputting: You need to output text as a plain string, but later may be a CSV, XML or PDF.

Examples of Strategy Pattern in Spring Framework and Java libraries

The strategy is a pattern that you often use using different librarians/frameworks in Java.
The best example of the strategy pattern is the Collection.sort() method that takes Comparator parameter. We do not need to change the sort() method to achieve different sorting results. We can just inject different comparators in runtime.

The next example is javax.servlet.Filter#doFilter() method.
In Spring Framework an example of using strategy is class: org.springframework.web.servlet.mvc.multiaction.MethodNameResolver

Summary

That’s all about the Strategy Design Pattern🙂
Link to Github with all examples: Github