The design problem
Let's imagine the following situation. There is a lot of monstrous conditional or switch statements to select an algorithm to be executed. Sometimes you have to add new algorithms to this complex structure. Another problem is the code begins to be duplicated in different contexts. How to add new algorithms easier and make the code more concise?
Fortunately, there is a special pattern named Strategy. It's one of the most used patterns in object-oriented design. It defines a family of algorithms, encapsulates each one in a separated class, and make them interchangeable within that family.
Introducing the strategy pattern
In Strategy pattern, algorithms executed in different branches are moved into their own classes called strategies, all of which implement a common interface. Strategies represent behaviour, not domain entities.
The pattern has several advantages
- it allows you to choose an algorithm (behaviour) at runtime;
- it isolated the code of algorithms from the other classes simplifying the addition of new algorithms;
- it lets the algorithm vary independently from clients that use it.
To use the pattern, you should do the following steps.
- Determine the common
Strategy
interface for a family of algorithms. It should contain one or more abstract methods. - Extract all algorithms into their own classes (concrete strategies). They should follow the
Strategy
interface. - Declare a special class called
Context
for storing a reference to a strategy. The context delegates execution to an instance of a concrete strategy through its interface, instead of implementing the behaviour itself.
Note, the interface here is not necessarily the interface in java. It can be a simple or an abstract class. The main thing is that it represents an abstract strategy that is inherited by concrete strategies.
The following diagram illustrates all elements of the pattern and their relations.
Client code should create an instance of
Context
, set a strategy and invoke a special method to start the execution. The invocation will be delegated to a concrete strategy that is referred by the strategy
field.In some cases, the
execute
method of a strategy implements the whole algorithm. In other cases, it implements only a part of the algorithm, and its basic part is in invoke
method of Context
. It may be useful when Strategy
interface provides several methods which implement independent parts of the general algorithm.Perhaps now this pattern looks a bit complicated and abstract. Let's consider an example.
An example: sending messages
Let's consider a simplified example. Suppose, an application needs to send messages to customers. There are different ways to send a message: via sms or email. In addition, new sending methods will be added in future (for instance, push sending). It would be nice to change the existing code as little as possible when adding new strategies.
According to the strategy pattern, we need to define a family of algorithms (sending methods). Each algorithm must encapsulate a logic to send a message using a concrete transport (sms, email).
Here is our hierarchy of sending methods:
interface SendingMethod {
void send(String from, String to, String msg);
}
class SmsSendingMethod implements SendingMethod {
@Override
public void send(String from, String to, String msg) {
System.out.println(String.format("send SMS from '%s' to '%s'", from, to));
}
}
class EmailSendingMethod implements SendingMethod {
@Override
public void send(String from, String to, String msg) {
System.out.println(String.format("Email from '%s' to '%s'", from, to));
}
}
Our
Context
is MessageSender
which reference a sending method and allows us to change the currently used method.class MessageSender {
private SendingMethod method;
// it may contain additional fields as well
public void setMethod(SendingMethod method) {
this.method = method;
}
public void send(String from, String to, String msg) {
this.method.send(from, to, msg);
}
}
In the client code, we should create an instance of
MessageSender
, set a sending method and invoke the method send with three string arguments. Also, we can change the sending method to another one.MessageSender sender = new MessageSender(); // create a message sender
sender.setMethod(new EmailSendingMethod()); // set a concrete sending method
sender.send("alice@gmail.com", "bob@gmail.com", "Hello!");
sender.setMethod(new SmsSendingMethod()); // set another sending method
sender.send("1-541-444-3333", "1-541-555-2222", "Hello!");
After starting, it outputs:
Email from 'alice@gmail.com' to 'bob@gmail.com'
send SMS from '1-541-444-3333' to '1-541-555-2222'
Of course, this example is greatly simplified. In a real application, you need to write more complex logic to send messages. But it demonstrates the main idea of the strategy pattern well.
Complicating the strategy pattern
For different reasons, you will meet more complex implementations of the strategy pattern.
1) Field and constructors. A concrete strategy may need own settings stored in fields. Here is a class
PushSendingMethod
with a setting field, we fill it in the constructor.class PushSendingMethod implements SendingMethod {
private final boolean magicFlag;
public PushSendingMethod(boolean magicFlag) {
this.magicFlag = magicFlag;
}
@Override
public void send(String from, String to, String msg) {
System.out.println(String.format("Send push from '%s' to '%s'", from, to));
}
}
sender.setMethod(new PushSendingMethod(true));
2) Different and unused arguments. Different strategies may need different arguments or use not all arguments defined in the overridden method. In addition, new strategies may need other arguments which will require a change in the entire hierarchy.
The simplest way to solve the problem is to pass an aggregate type containing necessary parameters. Each strategy will use only required fields of the aggregate.
Here is a rewritten class
SendingMethod
. The method send takes an instance of Message that contains different fields. Fields are public just for short.interface SendingMethod {
void send(Message message);
}
class Message {
public String from;
public String to;
public String title;
public String text;
public byte[] attachment; // attachment data just like an image or something else
}
3) Multiple methods. A strategy may have multiple methods. For instance,
PaymentStrategy
can contain methods to pay for an order and return the money. It's possible to separate the strategy into two independent ones. But sometimes it's better to have one strategy with multiple methods.Conclusion
The Strategy pattern is useful when we have multiple algorithms for performing a task and we want to choose any of the algorithms at runtime. This pattern is especially good when the number of algorithms increases because it isolated algorithms from client code. However, this design pattern complicates the system by adding multiple additional classes.