Java: The Strategy Pattern
This deep dive into the classic Strategy pattern covers how it works, example usage, and deciding how and when to use it in your projects.
Join the DZone community and get the full member experience.
Join For FreeSince its publication in 1994, the Gang of Four Design Patterns book has been one of the staple volumes in software engineering. While this book presents numerous pragmatic techniques for simplifying code and creating reusable components, one of the most prolific patterns it describes is the Strategy pattern. Apart from standalone implementations, this pattern is closely related to many other useful techniques and facilitates the implementation of many subsequent patterns.
In this article, we will explore the basic Strategy pattern and the fundamental principles that make for solid strategy implementations. We will also delve into some secondary topics, including the use of Dependency Injection (DI) to select a strategy implementation and a complete walkthrough of creating a payment system using the Strategy pattern. Note that all of the source code and examples used in this article can be found here on GitHub.
The Basic Pattern
The conditional statement is a core structure in nearly all software and in many cases, it serves a very simple function: To decide the algorithm used in a specific context. For example, if we are creating a payment system, a conditional might exist to decide on the payment method, such as cash or credit card. In this case, we supply the same information to both algorithms (namely, the payment amount) and each performs their respective operations to process the payments. In essence, we are creating a series of algorithms, selecting one, and executing it.
The purpose of the Strategy pattern is to encapsulate these algorithms into classes with the same interface. By using the same interface, we make the algorithms interchangeable with respect to the client using the algorithms and reduce the dependency on the algorithms from concrete algorithms to the interface that defines the algorithms. If we generalize this process, we obtain the following Unified Modeling Language (UML) class diagram:
The Context
class in this diagram represents the class that uses the algorithm, while the Strategy
interface represents the common contract for the algorithm. Each of the concrete classes that implement the Strategy
interface thus represents the various algorithm implementations. In order to gain an intuitive understanding of the Strategy
pattern, we can think of the Context
class as a desk, where the Strategy
interface represents a slot in the desk that accepts drawers. Additionally, each of the concrete Strategy
implementations can be thought of as actual drawers, where some are completely undivided, and others have various divider configurations; some may be good at storing papers, while others may be better at organizing pens, pencils, and paper clips. Each serves its own purpose and excels in a specific context, but vary only in their internal configuration: The external configuration of each drawer is the same so that it can be accepted by the desk.
This view of the Strategy pattern is nearly identical to that of interfaces in general: So long as we create an interface and depend only on that interface, we can substitute any of the implementations of that interface for one another. This similarity is not a coincidence, as the Strategy pattern is one of the most natural uses for interfaces. In the case of the Strategy pattern, we define a set of algorithms, select one, and then execute it. Since the Context
class is dependent on the concrete Strategy
implementations in terms of the Strategy
interface, executing any of the algorithms is accomplished in the same manner, with no knowledge on the part of the Context
class as to which Strategy
is being executed.
While we have defined our algorithm method above without arguments and with a void
return type, we can define it with any number of parameters and any return type, so long as the parameters contain all of the information that is required for the concrete Strategy
implementations. In cases where there are values needed for some algorithms and not others, we must still provide all of the parameters. Therefore, we end up with the union of parameters needed for each of the concrete Strategy
implementations. In the event that the number of parameters becomes unwieldy, we can consolidate them into a parameter object using the Introduce Parameter Object refactoring technique.
With a static view of the Strategy pattern established, the dynamic execution view of the Strategy pattern is almost trivial. When a Context
object is called and requires the execution of the desired algorithm, the current concrete Strategy
object associated with the Context
object is executed. Once the algorithm completes, control is returned to the Context
object. This concept is illustrated in the UML sequence diagram below:
With a static and dynamic view of the Strategy pattern established, we will explore a complete example that will illustrate both the design decisions that go into creating a set of strategies, as well as the benefits of the Strategy pattern in various scenarios.
Examples
While a theoretical understanding of the Strategy pattern is important for knowing how and when to implement the pattern in a system, seeing an example of the pattern often provides the intuitive understanding that is often missing with technical explanations. In this section, we will walk through the creation of a simple payment processing system, where a bill can be paid using a variety of payment methods selected at runtime.
A Payment System Walkthrough
Making payments is a common feature of any financial or commerce system, but handling the different payment types can be a challenge, especially with all of the payment types that are currently available (cash, credit, debit, Bitcoin, PayPal, etc.). While there are various intricacies involved in paying for an item with each of these payment methods, the basic interface is the same: Supply a cost and execute the payment for that cost.
Therefore, instead of scattering the logic for payment methods throughout our system, we can create a simple interface, PaymentMethod
, with a single method that accepts a cost in cents. We will then create three concrete strategy classes: (1) CreditCard
, (2) DebitCard
, and (3) Cash
. Since the CreditCard
and DebitCard
classes share the same fields (such as a name on the card, a number, etc.), we will create an abstract superclass named Card
to manage these fields. Each of the concrete Card
subclasses will then override the executeTransaction
method to perform card-specific logic, such as contacting a credit holder or bank, respectively.
We will also create a Bill
class that contains LineItem
objects that represent singular items to be purchased. The Bill
class will allow for clients to pay for all of the LineItem
objects contained within the Bill
by supplying a PaymentMethod
object to the pay
method. A UML class diagram for this conceptual model is illustrated below:
Note that the strategy interface, PaymentMethod
, is highlighted in blue, while the concrete strategy implements (namely CreditCard
, DebitCard
, and Cash
) are highlighted in purple. In order to tie these various classes together, we will create a main
method that accepts the desired type of payment as its first argument. For example, if the user supplies debit
as an argument while executing our payment system, the LineItem
objects for the Bill will be paid using a DebitCard
.
Starting with the payment strategy portion of the system, we define our strategy interface, PaymentMethod
, as follows:
public interface PaymentMethod {
public void pay(int cents);
}
Moving to the most simple concrete strategy, we implement the Cash
class as follows:
public class Cash implements PaymentMethod {
@Override
public void pay(int cents) {
System.out.println("Payed " + cents + " cents using cash");
}
}
Since our system is not connected to any real bank, we will simply print that we have paid for our bill using cash, which allows us to differentiate between the various payment methods. Before implementing the CreditCard
and DebitCard
classes, we must implement the Card
class:
public abstract class Card implements PaymentMethod {
private final String nameOnCard;
private final String number;
private final String cvv;
private final String expirationDate;
public Card(String nameOnCard, String number, String cvv, String expirationDate) {
this.nameOnCard = nameOnCard;
this.number = number;
this.cvv = cvv;
this.expirationDate = expirationDate;
}
@Override
public String toString() {
return MessageFormat.format("{0} card[name = {1}, number = {2}, CVV = {3}, expiration = {4}]", getType(), nameOnCard, number, cvv, expirationDate);
}
@Override
public void pay(int cents) {
System.out.println("Payed " + cents + " cents using " + toString());
}
protected abstract String getType();
protected abstract void executeTransaction(int cents);
}
Although there appears to be a great deal of logic in this class, a majority of the lines in the Card
class are focused on the fields associated with a card. Like the pay
method for the Cash
class, we will simply print that the bill has been paid using the appropriate card. In order to print the specific card type that is used to pay, we delegate to the subclass to provide the name of the card used (e.g. credit
or debit
). Likewise, we will allow the concrete subclasses to execute the transaction in a manner specific for each class using the executeTransaction
method. With these pieces in place, we can finally implement the CreditCard
and DebitCard
classes:
public class CreditCard extends Card {
public CreditCard(String nameOnCard, String number, String cvv, String expirationDate) {
super(nameOnCard, number, cvv, expirationDate);
}
@Override
protected String getType() {
return "credit";
}
@Override
protected void executeTransaction(int cents) {
// Contact credit holder to make transaction
}
}
public class DebitCard extends Card {
public DebitCard(String nameOnCard, String number, String cvv, String expirationDate) {
super(nameOnCard, number, cvv, expirationDate);
}
@Override
protected String getType() {
return "debit";
}
@Override
protected void executeTransaction(int cents) {
// Contact bank to execute transaction
}
}
Lastly, we must implement a means of selecting the correct concrete payment strategy. While we will delve deeper into this topic later in this article, but for now, we will create a simple conditional that returns the correct payment strategy depending on the argument supplied method (note that this class is an implementation of the Factory Method pattern):
public class PaymentMethodFactory {
public static PaymentMethod getPaymentMethod(String type) {
switch (type) {
case "credit":
return createCreditCard();
case "debit":
return createDebitCard();
case "cash":
return createCash();
}
throw new IllegalArgumentException();
}
public static CreditCard createCreditCard() {
return new CreditCard("John Doe", "4111111111111111", "123", "01/22");
}
public static DebitCard createDebitCard() {
return new DebitCard("John Doe", "4111111111111111", "123", "01/22");
}
public static Cash createCash() {
return new Cash();
}
}
With our strategies implemented, we can now implement the LineItem
and Bill
classes:
public class LineItem {
private String description;
private int costInCents;
public LineItem(String description, int costInCents) {
this.description = description;
this.costInCents = costInCents;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public int getCostInCents() {
return costInCents;
}
public void setCostInCents(int costInCents) {
this.costInCents = costInCents;
}
}
public class Bill {
private List<LineItem> lineItems = new ArrayList<>();
public void addLineItem(LineItem lineItem) {
lineItems.add(lineItem);
}
public void removeLineItem(LineItem lineItem) {
lineItems.remove(lineItem);
}
public int getCostInCents() {
return lineItems.stream().mapToInt(item -> item.getCostInCents()).sum();
}
public void pay(PaymentMethod method) {
method.pay(getCostInCents());
}
}
The final class we create is the Application
class, which ties the entire system together. Note that we will use test data (example line items) to illustrate how the payment system would work in a true production environment:
public class Application {
private static final int PAYMENT_TYPE_INDEX = 0;
public static void main (String[] args) {
Bill bill = new Bill();
bill.addLineItem(new LineItem("Milk", 200));
bill.addLineItem(new LineItem("Bread", 150));
bill.pay(PaymentMethodFactory.getPaymentMethod(args[PAYMENT_TYPE_INDEX]));
}
}
By inspecting the getPaymentMethod
of the PaymentMethodFactory
, we see that we have three possible arguments that can be accepted as valid arguments when running our application: (1) credit
, (2) debit
, and (3) cash
. The output for each is enumerated below:
credit:
Payed 350 cents using credit card[name = John Doe, number = 4111111111111111, CVV = 123, expiration = 01/22]
debit:
Payed 350 cents using debit card[name = John Doe, number = 4111111111111111, CVV = 123, expiration = 01/22]
cash:
Payed 350 cents using cash
Before proceeding to a discussion of the selection process for a strategy, we should note that although the formal Strategy pattern dictates that the selected strategy be a field of the Context
, this is not always the case in practice. Instead, as in the example above, the strategy is supplied to the Context
as a method argument and immediately executed. An alternative would have been to store the payment method strategy as a field and execute it at a later time, but this introduces some nuanced edge-cases, including checking that the strategy has been set (is not null
) prior to executing the strategy.
It is important to keep in mind that the strict definition of the Strategy pattern, or any pattern or technique in general, is not the only means of implementing it. Instead, judgment should be used in deciding when and how to vary a pattern to meet the needs of a specific situation. Although a transient deviation from the core of a pattern can cause confusing or faulty code, pedantic adherence to a strict pattern can cause just as many subtle issues.
Selecting the Appropriate Strategy
One of the most important aspects of a Strategy pattern implementation is the selection of the concrete strategy to be executed at runtime. In general, there are two techniques for selecting the appropriate strategy to execute: (1) a run-time selection or (2) a static selection.
Runtime Selection
Selecting the appropriate strategy at run-time provides the greatest level of flexibility, but it can also introduce a great deal of complexity if not properly managed.
Conditional Logic
A common means of selecting the appropriate strategy at run-time is using some token (i.e. a supplied string, integer, etc.) and generating the concrete strategy object that corresponds to the supplied token. In the example above, we abstracted this selection process into the PaymentMethodFactory
class.
In general, a conditional run-time selection will require logic whose complexity is proportional to the number of possible concrete strategies. For example, if there are only a few concrete strategies, such as in the example above, the conditional logic is simple, but if the number of possible strategies grows, the conditional grows in kind.
Reflection
Using conditional, though, assumes that we know the all of the possible concrete strategies that can be supplied to the Context
. In many cases, the complete list of strategies will not be known a priori. As a matter of fact, not knowing all the possible strategies may be an asset, as it allows another component to create a new concrete strategy implementation. For example, we can create another payment method, such as Bitcoin, and supply it to our Bill
class as we would any of the existing PaymentMethod
implementations.
If all of the possible strategies are not known a priori, we can use reflection to dynamically create an object that corresponds to the desired concrete strategy using only the name of the concrete strategy. For example, we can create another PaymentMethod
factory that generates PaymentMethod
objects based on the supplied concrete class name:
public class RunTimeReflectionPaymentMethodFactory {
public static Optional<PaymentMethod> getPaymentMethod(String qualifiedName) {
try {
Class<?> clazz = Class.forName(qualifiedName);
PaymentMethod method = (PaymentMethod) clazz.newInstance();
return Optional.of(method);
}
catch (ClassNotFoundException | InstantiationException | IllegalAccessException | ClassCastException e) {
return Optional.empty();
}
}
}
We can then use this factory method to generate payment methods at run-time:
public static void main (String[] args) {
Bill bill = new Bill();
bill.addLineItem(new LineItem("Milk", 200));
bill.addLineItem(new LineItem("Bread", 150));
Optional<PaymentMethod> paymentMethod = RunTimeReflectionPaymentMethodFactory
.getPaymentMethod("com.dzone.albanoj2.examples.patterns.strategy.Cash");
paymentMethod.ifPresent(method -> bill.pay(method));
}
In this case, we paid for the Bill
object using the Cash payment method. Note that the class name supplied to the factory method must be fully qualified (contain both the package name and the class name). Also note that the return type of the factory method is Optiona<PaymentMethod>
, rather than simply PaymentMethod
. The Optional
type is chosen in order to guard against a class name that could not be instantiated (i.e. the provided class name does not exist on the classpath) or if the supplied class name does not correspond to a PaymentMethod
(i.e. the cast to PaymentMethod
fails). In either failure case, the returned value resolves to an empty Optional
and the pay
method of the Bill
object is not executed.
There is a major caveat when instantiating the concrete PaymentMethod
class as in the above example: This technique assumes that there exists a no-argument constructor for the class to be instantiated. If there are arguments, such as with our CreditCard
class, we must adjust our reflection code to account for the arguments (see this StackOverflow post for more information). Note that doing so will cause a discrepancy if all of the concrete strategies do not have the same number of constructor arguments. Due to this limitation, reflection should only be used in simple cases, or cases where the nature of the strategy implementations is precisely known (i.e. it is known a priori that all strategy implementations will have no-argument constructors).
Static Selection
In contrast to run-time selection, we can also perform static selections of the PaymentMethod
. In this section, we will use the term static to denote that the PaymentMethod
is selected once when the application starts-up and is never reinitialized for the duration of the execution of the application. This does not mean that the selection is made at compile-time, but rather, that the selection is made once during the initialization of the application and never changed thereafter.
Reflection
Although we have seen an instance of reflection used to instantiate a PaymentMethod
object at run-time, we can also make a static selection of the concrete PaymentMethod
by placing the fully qualified name of the concrete PaymentMethod
in a configuration file. In this case, we will embed the selected PaymentMethod
in a properties file and create a new factory class as follows:
public class StaticReflectionPaymentMethodFactory {
public Optional<PaymentMethod> getPaymentMethod() {
try {
Optional<String> className = getPaymentMethodClassName();
if (className.isPresent()) {
Class<?> clazz = Class.forName(className.get());
PaymentMethod method = (PaymentMethod) clazz.newInstance();
return Optional.of(method);
}
else {
return Optional.empty();
}
}
catch (ClassNotFoundException | InstantiationException | IllegalAccessException | ClassCastException e) {
e.printStackTrace();
return Optional.empty();
}
}
private Optional<String> getPaymentMethodClassName() {
try {
Properties paymentConfig = new Properties();
InputStream configFile = StaticReflectionPaymentMethodFactory.class.getResourceAsStream("/payment.config.properties");
paymentConfig.load(configFile);
return Optional.of(paymentConfig.getProperty("paymentMethod.className"));
}
catch (IOException e) {
e.printStackTrace();
return Optional.empty();
}
}
}
We then define a configuration file and place it on the classpath:
paymentMethod.className=com.dzone.albanoj2.examples.patterns.strategy.Cash
We can then execute this code using the following main method:
public static void main (String[] args) {
Bill bill = new Bill();
bill.addLineItem(new LineItem("Milk", 200));
bill.addLineItem(new LineItem("Bread", 150));
Optional<PaymentMethod> staticPaymentMethod = StaticReflectionPaymentMethodFactory.getPaymentMethod();
staticPaymentMethod.ifPresent(method -> bill.pay(method));
}
Just as in the previous case, the Bill
object will be paid for using the Cash
payment method. Note, though, that this technique still suffers from the same constructor argument impediment as the run-time reflection example.
Dependency Injection
While Dependency Injection (DI) is a deep topic, we would be remiss if we did not cover it as a technique for statically selecting a strategy implementation. The core premise to DI is that we declare fields of a class to expect a dependency and in another location, define the actual implementation type to use to resolve these dependencies. In this section, we will use Spring as our DI framework, although we will not dive into the details of how Spring DI works. For more information on this topic, see Intro to Inversion Control and Dependency Injection with Spring.
In order to allow for our dependencies (called beans in Spring) to be injected, we must declare our desire for a bean to be injected into our class using the @Autowired
annotation. For demonstration, we will create a new class, DependencyInjectedBill
that will mirror our original Bill
, expect that our pay
method will not take in any parameters. Instead, our DependencyInjectedBill
class will have a PaymentMethod
field injected by the Spring container:
public class DependencyInjectedBill extends Bill {
@Autowired
private PaymentMethod paymentMethod;
private List<LineItem> lineItems = new ArrayList<>();
public void addLineItem(LineItem lineItem) {
lineItems.add(lineItem);
}
public void removeLineItem(LineItem lineItem) {
lineItems.remove(lineItem);
}
public int getCostInCents() {
return lineItems.stream().mapToInt(item -> item.getCostInCents()).sum();
}
public void pay() {
paymentMethod.pay(getCostInCents());
}
}
With our new DependencyInjectedBill
established, we now have to specify how our bean will be wired by the Spring container. For this example, we will use the Spring Extensible Markup Language (XML) specification technique. The major advantage here to using an XML-based specification over a Java-based configuration is that we can change the data associated with our bean without having to recompile the application. For example, if we want to change the name on the CreditCard
that will be injected, we simply change the constructor arguments for our bean in the XML specification. The resulting XML specification is illustrated below:
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
<context:annotation-config />
<bean class="com.dzone.albanoj2.examples.patterns.strategy.di.DependencyInjectedBill" />
<bean class="com.dzone.albanoj2.examples.patterns.strategy.CreditCard">
<constructor-arg value="Jane Doe" />
<constructor-arg value="7111111111111111" />
<constructor-arg value="123" />
<constructor-arg value="05/24" />
</bean>
</beans>
The <context:annotation-config />
statement allows for annotation-based configuration, such as @Autowired
, to be used. This reduces the verbosity of the auto-wiring process, allowing us to simply use the @Autowired
annotation instead of creating an explicit setter for our PaymentMethod
field and defining the property name and value in the XML configuration for that field. The remainder of the XML configuration defines the beans that will be injected, including the DependencyInjectedBill
bean. Note that the constructor arguments are mapped by position, with the first constructor-arg
definition being mapped to the first parameter of the CreditCard
constructor and the last definition being mapped to the last parameter of the constructor.
The last steps to pull all of the DI steps together is to create the Spring container (called a context in Spring), load the configuration, and then request an initialized DependencyInjectedBill
bean from the context. By requesting the initialized bean, the Spring context will resolve the auto-wired dependency and provide us with an initialized DependencyInjectedBill
that contains the PaymentMethod
field object specified in our XML specification. This loading, initialization, and execution process is illustrated in the listing below:
public static void main (String[] args) {
BeanFactory factory = (BeanFactory) new ClassPathXmlApplicationContext("META-INF/beans.xml");
DependencyInjectedBill diBill = (DependencyInjectedBill) factory
.getBean("com.dzone.albanoj2.examples.patterns.strategy.di.DependencyInjectedBill");
diBill.addLineItem(new LineItem("Eggs", 350));
diBill.addLineItem(new LineItem("Cheese", 150));
diBill.pay();
}
If we execute this method, we see the following output:
INFO: Refreshing org.springframework.context.support.ClassPathXmlApplicationContext@75626172: startup date [Sun Oct 22 06:49:28 EDT 2017]; root of context hierarchy
Oct 22, 2017 6:49:28 AM org.springframework.beans.factory.xml.XmlBeanDefinitionReader loadBeanDefinitions
INFO: Loading XML bean definitions from class path resource [META-INF/beans.xml]
Payed 500 cents using credit card[name = Jane Doe, number = 7111111111111111, CVV = 123, expiration = 05/24]
The first three lines specify that the Spring container has started and loaded our XML specification for the bean definitions. The last line shows that our PaymentMethod
field has been successfully autowired and used to pay for the DependencyInjectedBill
object. Note that if we wanted to change any of the data associated with the CreditCard
object, we can simply change the values of the constructor arguments in our XML specification. For example, if want to change the name on the card, we can change our XML specification to be the following:
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
<context:annotation-config />
<bean class="com.dzone.albanoj2.examples.patterns.strategy.di.DependencyInjectedBill" />
<bean class="com.dzone.albanoj2.examples.patterns.strategy.CreditCard">
<constructor-arg value="Justin Albano" />
<constructor-arg value="7111111111111111" />
<constructor-arg value="123" />
<constructor-arg value="05/24" />
</bean>
</beans>
If we rerun our application, we receive the following output:
INFO: Refreshing org.springframework.context.support.ClassPathXmlApplicationContext@75626172: startup date [Sun Oct 22 07:05:40 EDT 2017]; root of context hierarchy
Oct 22, 2017 7:05:40 AM org.springframework.beans.factory.xml.XmlBeanDefinitionReader loadBeanDefinitions
INFO: Loading XML bean definitions from class path resource [META-INF/beans.xml]
Payed 500 cents using credit card[name = Justin Albano, number = 7111111111111111, CVV = 123, expiration = 05/24]
The advantage of using the DI technique, coupled with an XML-based specification, is that we do not have to recompile the entire application to change the selected strategy. Instead, we simply change the values in our specification and restart the application.
Conclusion
Since it was codified in 1994, the Strategy pattern has been one of the most prolific patterns to sweep the object-oriented programming world. In Java alone, it can be seen in various locations through the standard Java classes, as well as in countless other frameworks, libraries, and applications. In this article, we covered the basics of the pattern, including its textbook structure and the fundamental points that should be considered when implementing the pattern. We also covered a detailed example of the pattern in use, as well as various techniques for properly selecting a strategy. While this pattern is basic in its appearance, the possibilities for its use are nearly endless and only experience will provide the knowledge of when and how to properly apply the Strategy pattern.
Opinions expressed by DZone contributors are their own.
Comments