1. Overview

Should strategy classes be stateless or stateful? This is a question related to API design and may have different meanings for different people. So let's be more clear and ask the following questions:

  • Should we have one strategy instance or construct a new one for each invocation?
  • How the parameter set affects the lifetime of an interface?
  • Should we add all related parameters to the strategy method? Or can some information be provided during construction time of strategy class?

In this tutorial, we'll iterate over some examples to answer these questions.

2. Capturing the Runtime Data

We'll first look at how we can capture data that changes at runtime.

We want to print a String. The Printer interface will define this operation. Even for this simple operation, we can design two different interfaces.

As the first option, Printer has the print() method which doesn't take any argument:

public interface Printer {

    void print();
}

When we implement Printer, the concrete class should store the String value as an instance variable:

public class PrinterImpl implements Printer {

    private final String value;

    public PrinterImpl(String value) {
        this.value = value;
    }

    @Override
    public void print() {
        System.out.println(value);
    }
}

Here, we have the PrinterImpl class which takes a String value in the constructor. We need to create a new PrinterImpl instance for each print operation. Because the value is given at runtime and we aren't capturing this runtime value as a method parameter.

Let's continue with the second interface.

PrinterWithParameter has the print(String value) method. In this case, we're capturing the runtime value as a method parameter:

public interface PrinterWithParameter {

    void print(String value);
}

When we implement the PrinterWithParameter interface, the resulting class doesn't need to store any instance data:

public class PrinterWithParameterImpl implements PrinterWithParameter {

    @Override
    public void print(String value) {
        System.out.println(value);
    }
}

Here, we have the PrinterWithParameterImpl class. A single instance is sufficient to handle all print operations.

To conclude, capturing runtime data in the method parameters seems more appropriate in terms of resource consumption and performance. If the runtime variable is also applicable to other possible implementations, we better define it as a method parameter.

3. Selecting the Parameter Set

Now, we'll investigate how the parameter set affects the lifetime of a strategy interface.

We have an algorithm to filter a word, WordFilter. We can filter the words using a whitelist, using a blacklist or by some other criteria:

public interface WordFilter {

    void filter(String word, List<String> whiteList, List<String> blackList);
}

We're defining the filter method with the whiteList and blackList parameters. Assuming that we'll have two implementations - BlackListWordFilter and WhiteListWordFilter -, this method signature satisfies our requirements.

However, it has some drawbacks. First of all, if the client code uses just one of the implementations, we'll force the client to provide redundant data. Even more, the client may not have the required data and can supply just null or empty value.

Secondly, the method signature is heavily dependent on the implementations. If we add another implementation, we may also need to change the interface to accommodate another parameter.

So the lifetime of this interface seems short. One enhancement is encapsulating the method parameters in an object:

public class WordFilterOptions {

    private List<String> whiteList;
    private List<String> blackList;

    public List<String> getWhiteList() {
        return whiteList;
    }

    public void setWhiteList(List<String> whiteList) {
        this.whiteList = whiteList;
    }

    public List<String> getBlackList() {
        return blackList;
    }

    public void setBlackList(List<String> blackList) {
        this.blackList = blackList;
    }
}
public interface WordFilter {

    boolean filter(String word, WordFilterOptions wordFilterOptions);
}

This way, the addition of new parameters will only affect WordFilterOptions, not the interface or its implementations.

Obviously, this change has encapsulated some of the domain logic and improved the design. But it has still fundamental drawbacks. The interface is still dependent on the implementation details. Moreover, if the values in WordFilterOptions are always the same for some strategy class, then we can also define them inside that strategy class. In effect, we're creating an additional overhead - passing these values in every invocation.

4. Determining the Method and Constructor Parameters

Let's continue investigating whether we should add all related parameters to the strategy method.

If we use the previous example, a better approach for WordFilter is changing its method signature:

public interface WordFilter {

    boolean filter(String word);
}

Then the strategy classes can gather other required data during the construction time.

public class WhiteListWordFilter implements WordFilter {

    private final List<String> whiteList;

    public WhiteListWordFilter(List<String> whiteList) {
        this.whiteList = Collections.unmodifiableList(whiteList);
    }

    @Override
    public boolean filter(String word) {
        return whiteList.contains(word);
    }
}
public class BlackListWordFilter implements WordFilter {

    private final List<String> blackList;

    public BlackListWordFilter(List<String> blackList) {
        this.blackList = Collections.unmodifiableList(blackList);
    }

    @Override
    public boolean filter(String word) {
        return !blackList.contains(word);
    }
}

These strategy classes have an internal state since they hold some data. For these examples, the state isn't changing over time. And generally, it shouldn't.

There can be a single instance for each strategy class or a new instance can be created per invocation. If a single instance will handle all calls, we must evaluate the thread-safety of the class.

5. Summary

In this tutorial, we've looked at some design decisions when implementing the strategy pattern.

As always, the source code for all examples is available on Github.