Strategy Design Pattern

Definition

Define a family of algorithms, encapsulate each one, and make them interchangeable. Strategy lets the algorithm vary independently from clients that use it.

Introduction

The Strategy Design Pattern falls under the Behavioral Patterns. It is one of the most commonly used design patterns in the software development world. You can find the application of this pattern a numerous number of times in a code base or a project.

We will start with an example.

Imagine an application that works with different kinds of Text documents (say XML, JSON and Plain Text). We have to parse each kind of text in a certain way. We are looking to build a parser that can parse these text documents.

Simple Solution

Let us create a class called SimpleTextParser that is responsible for parsing a Text given to it. It has to parse the text according to the format of the text. Assume the input is a simple POJO called Text which encapsulates the data (as a String) and the format of the data. The output is represented by the ParsedText class that holds the result of the parsing.

public class Text {
    private String data;
    private TextFormat textFormat;

    public Text(String data, TextFormat textFormat) {
        this.data = data;
        this.textFormat = textFormat;
    }

    public String getData() {
        return data;
    }

    public TextFormat getTextFormat() {
        return textFormat;
    }
}

public enum TextFormat {
    XML,
    JSON,
    PLAIN_TEXT;
}

public class SimpleTextParser {
    public ParsedText parse(Text rawText) {
        TextFormat textFormat = rawText.getTextFormat();
        String rawData = rawText.getData();
        ParsedText parsedText;
        if (textFormat == TextFormat.JSON) {
            /*
              logic to parse rawData JSON
            */

            //Construct result
            parsedText = new ParsedText();
        } else if (textFormat == TextFormat.XML) {
             /*
              logic to parse rawData JSON
             */

            //Construct result
            parsedText = new ParsedText();
        } else if (textFormat == TextFormat.PLAIN_TEXT) {
             /*
              logic to parse rawData JSON
             */

            //Construct result
            parsedText = new ParsedText();
        } else {
            throw new RuntimeException("Unsupported TextFormat received :" + textFormat);
        }
        return parsedText;
    }
}

This is undesirable for the following reasons:

  • The SimpleTextParser code (client code) has become complex as it is responsible for parsing all types of Texts and it will be difficult to maintain.
  • To support a new text format, this class needs to be changed, and this violates the Open-Closed Principle (OCP) principle.
  • The individual text parsing algorithm cannot be reused as it is embedded in the client code.

Using the Strategy Pattern

We can avoid these problems by defining classes that encapsulate different Text parsing algorithms. An algorithm that is encapsulated in this way is called a strategy.

Thus, the logic or the actual text parsing algorithms are not implemented by the SimpleTextParser class. We create an abstraction for parsing any text (TextParser) and the individual text parsing algorithms are implemented separately by implementing the TextParser’s contract.

The SimpleTextParser class maintains a reference to a TextParser object. The SimpleTextParser forwards the responsibility of parsing to the TextParser object it holds. (Note: The SimpleTextParser class is also called as the Context class)

The client of the SimpleTextParser class specifies which TextParser should be used by passing an instance of a TextParser into the SimpleTextParser class when the SimpleTextParser is created (in its constructor or using a setter method). When it has to parse a text, it just delegates to the underlying TextParser object it has.

Structure

Participants

Strategy (Compositor)

  • An interface that is common to all the supported algorithms. The Context uses this interface to call the algorithm defined by a ConcreteStrategy.

ConcreteStrategy 

  • Provides an implementation of the Strategy interface (PlainTextParser, JsonTextParser and XmlTextParser)

Context (Composition)

  • It is composed with a ConcreteStrategy object. It maintains a reference to a Strategy object.

Implementation

public interface TextParser {
    ParsedText parse(Text rawText);
}
public class JsonTextParser implements TextParser {
    @Override
    public ParsedText parse(Text rawText) {
        ParsedText parsedText;
        /*
          logic to parse JSON
         */

        //Construct result
        parsedText = new ParsedText();
        return parsedText;
    }
}

public class PlainTextParser implements TextParser {
    @Override
    public ParsedText parse(Text rawText) {
        ParsedText parsedText;
        /*
          logic to parse PLAIN_TEXT
         */

        //Construct result
        parsedText = new ParsedText();
        return parsedText;
    }
}

public class XmlTextParser implements TextParser {
    @Override
    public ParsedText parse(Text rawText) {
        ParsedText parsedText;
        /*
          logic to parse XML
         */

        //Construct result
        parsedText = new ParsedText();
        return parsedText;
    }
}
public class SimpleTextParser {
    private TextParser textParser;

    public SimpleTextParser(TextParser textParser) {
        this.textParser = textParser;
    }
    public ParsedText parse(Text rawText) {
        //Just delegate to the underlying TextParser.
        return textParser.parse(rawText);
    }
}

When to use the Strategy Pattern

  • When you have many related class that differ only in their behaviour. In this example, the way to parse a text is the behaviour that varies from one text format to another. Hence we abstracted that out by providing a common contract/interface for parsing any Text.
  • When you have multiple conditional statements at a place. Each of the related conditional branches can be moved into their own Strategy class.
  • You need different variants of an algorithm. You might define algorithms reflecting different space/time trade-offs. Example: One implementation might cache the results while the other always perform some expensive operation to compute a value.

Strategy Pattern – Pros

  • Families of related algorithms: All related implementations of strategies become a hierarchy (define a family of algorithms or behaviours) that is reusable.
  • No subclassing: We could have used inheritance to solve the problem (i.e, created subclasses of the Context class to give it different behaviours). But this hard-wires the behaviour into Context which makes the Context harder to understand, maintain, and extend. By encapsulating the algorithm in separate Strategy classes we can vary the algorithm independently of its context, making it easier to switch, understand, and extend.
  • Strategies eliminate conditional statements: Encapsulating the behaviour in separate Strategy classes eliminates conditional statements.
  • A choice of implementations: Strategies can provide different implementations of the same behaviour. We can choose an implementation depending on the need.

Strategy Pattern – Cons

  • Clients must be aware of different Strategies: A client must understand how Strategies differ before it can select the appropriate one. In our example, we need to compose the Context (SimpleTextParser) with an implementation of a TextParser) Note: There are ways to overcome this problem (using a Factory pattern).
  • Communication overhead between Strategy and Context: The Strategy pattern is possible only if can come up with a common interface or contract (TextParser) as we did. If each of the different implementations needs different inputs, then the common contract might need to take them all as parameters. This implies that the concrete strategies won’t use all the information passed to them through this interface. To avoid this, the Context can pass itself as an argument to the strategy. But this needs accessors(getters) on the Context for exposing the inputs to the strategies. This will result in a tighter coupling between the Strategy and the Context.
  • Increased number of objects: Strategies increase the number of classes/objects in an application. If the implementations are small/trivial then we can create lambda expressions for the implementations and this avoids the need to create classes.

Design principles used

Conclusion

The Strategy pattern is the go-to pattern when we have multiple implementations (or algorithm) that could be used in a place in our code. We separate out the part that could vary and encapsulate it. A separate contract is created for the Strategy and hence we no longer tie the code using the Strategy pattern with the implementation details. We can pass a particular implementation of the contract (Strategy) into the (Context) code and it can vary at runtime by creating different Context objects – each composed with different implementations of the Strategy.

References and Resources

  1. Head First Design Patterns: A Brain-Friendly Guide by Eric Freeman and Elisabeth Robson.
  2. Design Patterns: Elements of Reusable Object-Oriented Software by Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides.
Strategy Pattern
Java Strategy Design Pattern

Leave a Reply