Some short thoughts on strategy pattern
„(..) all of life is one of two things: strategy or organization.”
― Tom Peters
Strategy is one of the most useful design patterns, which allows us to change algorithms or implementations of an operation during runtime. Typical use cases include validating or parsing data depending on some decisions made on the fly. And although the pattern is widely described in various sources and well known amongst programmers I think there is still a missing part in great number of examples we can find. Let me explain what I have in mind…
Problem
To describe the issue properly we need to first introduce some domain for which strategy will be a good choice. So let’s assume that we are developing a web application and we need to integrate multiple customers or service providers. All of them will send us some details about transactions we need to perform, but in different formats — to keep it simple let them be XML and JSON. For various reasons we decide to model the transaction payload as follows:
public class TransactionPayload {
private final String transactionId;
private final byte[] content;
private final TransactionFormat format;
public TransactionPayload(String transactionId,
byte[] content,
TransactionFormat format) {
this.transactionId = transactionId;
this.content = content;
this.format = format;
}
// getters ommited for brevity
}
The natural need in such a case is to be able to translate transactions from different formats into one common object, which will allow us to apply business logic transparently. It is not necessary in this article to present this common class internals, but it will help if we name it. So let it be TransactionDescription
.
It should be visible already that the problem described is perfect case for strategy pattern application. We need a class which will be able to validate TransactionPayload
, probably save it in database, translate it to common format and finally handle it. Because only the translation part of the code will be different, it would be nice to delegate it to different classes. So following the examples of strategy usage we would most probably end up with a parser interface:
public interface TransactionPayloadParser {
TransactionDescription parse(TransactionPayload transactionPayload);
}
This interface will be implemented in our case by two classes — XMLTransactionPayloadParser
and JSONTransactionPayloadParser
. The class handling all transaction payloads itself could look then as below:
public class TransactionHandler {
private TransactionPayloadParser transactionParser;
private final TransactionRepository transactionRepository;
// some other members
public TransactionHandler(TransactionPayloadParser transactionParser,
TransactionRepository transactionRepository) {
this.transactionParser = transactionParser;
this.transactionRepository = transactionRepository;
}
public void handleTransaction(TransactionPayload transactionPayload) {
// validate payload
TransactionDescription parsedTransaction = transactionParser.parse(
transactionPayload
);
transactionRepository.save(parsedTransaction);
// perform business logic
}
public void setTransactionParser(TransactionPayloadParser transactionParser) {
this.transactionParser = transactionParser;
}
}
All of these seems to be perfectly fine and are in the line with the sources describing strategy pattern. There is only one if — in most of the cases the exemplary usage is overly simplified and looks similar to the code below:
public static void main(String... args) {
TransactionPayload xmlTransaction = testXMLTransaction();
TransactionPayload jsonTransaction = testJSONTransaction();
TransactionHandler transactionHandler = new TransactionHandler(
new XMLTransactionPayloadParser(),
new TransactionRepository()
);
// prints "Parsing XML transaction!"
transactionHandler.handleTransaction(xmlTransaction);
// changing the strategy
transactionHandler.setTransactionParser(new JSONTransactionPayloadParser());
// prints "Parsing JSON transaction!"
transactionHandler.handleTransaction(xmlTransaction);
// well... it works!
}
The problem here is that such code does not present how the objects related to the pattern should be managed or created in a real use case. And I think this can be a great source of confusion, especially for people just getting to know design patterns in general.
Solution
So we have the problem sketched, but you probably wonder what could be the solution. Well, we need to change our approach to the strategy a bit. The example above moved the responsibility to choose right strategy from TransactionHandler
to the programmer using this class. Frankly speaking, if I would be the client of this small API, I would not like to care about choosing right strategy for TransactionHandler
. The most convenient situation would be if it could be able to choose the right strategy for itself.
The first idea to accomplish this is to keep a collection of TransactionPayloadParsers
inside TransactionHandler
. The best data structure seems to be a Map
, which will allow us easily to choose right parser for the type of transaction:
public class TransactionHandler {
private Map<TransactionFormat, TransactionPayloadParser> parserRegistry = new HashMap<>();
{
parserRegistry.put(TransactionFormat.XML, new XMLTransactionPayloadParser());
parserRegistry.put(TransactionFormat.JSON, new JSONTransactionPayloadParser());
}
private final TransactionRepository transactionRepository;
// some other members
public TransactionHandler(TransactionPayloadRepository transactionRepository) {
this.transactionRepository = transactionRepository;
}
public void handleTransaction(TransactionPayload transactionPayload) {
// validate payload
TransactionPayloadParser transactionParser = parserRegistry.getOrDefault(
transactionPayload.getFormat(),
p -> {
throw new IllegalArgumentException("Unrecognized transaction format: " +
transactionPayload.getFormat());
}
);
TransactionDescription parsedTransaction = transactionParser.parse(transactionPayload);
transactionRepository.save(parsedTransaction);
// perform business logic
}
}
What we have achieved with this rather small change is actually a lot — using TransactionHandler
is now a lot more easier and does not require wondering which parser will it need in particular situation.
The change worth considering is moving collection of parsers into separate utility class for future reuse. While doing this we can also get rid of initializer block in favor of accepting a list of parsers in the constructor. To do this we need to change the parser interface a little:
public interface TransactionPayloadParser {
TransactionDescription parse(TransactionPayload transactionPayload);
// returns information about transaction format supported
TransactionFormat supportedFormat();
}
Now we can implement a reusable TransactionPayloadParserProvider
:
public class TransactionPayloadParserProvider {
private Map<TransactionFormat, TransactionPayloadParser> parserRegistry = new HashMap<>();
public TransactionPayloadParserProvider(List<TransactionPayloadParser> parsers) {
parsers.forEach(parser -> parserRegistry.put(parser.supportedFormat(), parser));
}
public TransactionPayloadParser getParserFor(TransactionPayload transactionPayload) {
TransactionPayloadParser parser = parserRegistry.get(transactionPayload.getFormat());
if(nonNull(parser)) {
return parser;
} else {
throw new IllegalArgumentException("Unrecognized transaction format: " +
transactionPayload.getFormat());
}
}
}
Now instead of keeping a raw Map
inside TransactionHandler
we can use our newly created class and do not care about picking the right strategy. We still need to supply a list of strategies somewhere, but in comparison to the example from the main method in the previous section we have all of the parsers sitting nicely gathered in one place.
Using Spring
With a little bit help from Spring we can make our lives even easier. Instead of manually creating list of parsers to satisfy constructor of TransactionPayloadParserProvider
we can let Spring do almost all the work for us. Provided that all our parser implementations are Spring managed beans we can introduce following change to our provider:
@Autowired
public TransactionPayloadParserProvider(List<TransactionPayloadParser> parsers) {
parsers.forEach(parser -> parserRegistry.put(parser.supportedFormat(), parser));
}
From now on all parser types known to Spring, supporting different transaction formats, will be automatically injected into our TransactionPayloadParserProvider
which in turn could be automatically injected into TransactionHandler
:
@Component
public class TransactionHandler {
private final TransactionPayloadParserProvider parserProvider;
private final TransactionRepository transactionRepository;
// some other members
@Autowired
public TransactionHandler(TransactionPayloadParserProvider parserProvider,
TransactionPayloadRepository transactionRepository) {
this.parserProvider = parserProvider;
this.transactionRepository = transactionRepository;
}
public void handleTransaction(TransactionPayload transactionPayload) {
// validate payload
TransactionPayloadParser parser = parserProvider.getParserFor(transactionPayload);
TransactionDescription parsedTransaction = parser.parse(transactionPayload);
transactionRepository.save(parsedTransaction);
// perform business logic
}
}
The life seems so easy right now, does it not?
Summary
Like with most of the concepts, which seem really simple at first glance, strategy pattern is worth some second thoughts. Although we can easily find a lot of exemplary implementations, many of them remain silent about organizing our strategies and managing their life cycle.
I hope that in this short post I have filled this gap and shown how strategies can be managed in our code — both in pure Java and in Spring. Happy coding!
Thanks!
Good article. Strategy pattern should be changed. We need a “StrategyManager” that will choose appropriate strategy, without a POJO need to know anything about it.