The strategy pattern lets you make "pluggable algorithms", so clients have different behavior without having different code themselves. We often use it to capture the "consequence in code" of some condition, which we can then let other code use without re-testing the condition.
Here’s a little java snippet:
dimension = horizontal ? width : height
If you’re not familiar with ternary operations, what this says is "if horizontal is true, use the width, otherwise use the height".
That snippet occurs in the context of a SplitPane (from JavaFx). A SplitPane holds N other panes, and its job is to lay them out in a row (or column!) with a draggable divider between them. The user drags the divider, and the child panes resize accordingly.
If you think about that job, you realize that all the layout math is basically identical for horizontal splitters or for vertical ones, with the only difference being whether its all based on the widths of things or all based on the heights of things.
The description of that ternary up there in domain terms: getTheImportantDimension. In the actual code, it and a bunch of variants of it occur many times. We want the important dimension of the SplitPane, its child panes, and its dividers.
To complicate the picture, JavaFx uses a bottom-up style for layouts. Those child panes have current dimensions, preferred dimensions, max dimensions, and min dimensions, and the layout algorithm tries to honor all that (and has special code for when it can’t).
So that condition field, horizontal, is used in ternaries like the one I showed, and occasionally in direct if’s, oh, idunno, a few dozen times.
There are two consequences of this: 1) The algorithm is quite heavily obscured, because the ternaries add so much noise to the signal. 2) The running code is actually testing the horizontal flag a 100 or more times during a layout, even though it doesn’t change.
There are one or two places in the code where the ternary is reversed, which also adds smoke: occasionally, the code has to also do/know something about the un-important dimension. That’s rare, but it’s in there, and the geek’s just gonna have to be on the lookout for it.
Now, all of these dimensions are coming off a base class way up the hierarchy, node. So the real ternary’s are getting the actual values out of the node:
horizontal ? node.getWidth() : node.getHeight()
So, first, imagine a DimensionGetter class, with two API’s (for the moment):
Cool! We initialize it with horizontal, and the bodies of those methods are exactly that ternary.
We can then add more API’s for all the different things:
maxImportant minImportant preferredImportant
And we can create just one of these, right at the moment(s) where horizontal is set, we instantiate a DimensionGetter, pass it the flag, and use that one guy all over.
Now if we do this, we’ve improved the readability of our SplitPane quite a bit. The ternary noise is gone, we have a clear label for whether this getter is the normal "important" case or the rare "unimportant" case.
What we haven’t done, though, is improved our runtime or simplified our control flow. We’re still checking that horizontal flag in every call, and if performance is your thing, we’ve now also added an additional function call.
Now, though, the stage is set for the Strategy pattern. Make DimensionGetter an interface, and derive two classes off it. One implements all those methods as if horizontal were true, the other as if horizontal were false. No more ternaries, even internally.
When we change our orientation, or far more commonly when we set it right at constructor time, we test the value one time, and assign the correct DimensionGetter to a field, side-by-side with horizontal itself.
What we have done is implement the Strategy pattern. The two different implementations of DimensionGetter represent two different strategies for getting the important/unimportant dimensions out of the nodes for our algorithm.
We started by keeping the condition, horizontal, in a field. We ended by keeping the consequence of that condition, a particular instance of getter. Now the client makes straight-line calls to the consequence rather than re-checking the condition.
There are a lot of situations where Strategy is a useful way to shape code. And of course, once you’ve succeeded with it a few times, you’ll see it upfront: you won’t just be refactoring to it, you’ll have designed with it from outset.
Some notes to help you think about it all.
First, in our case the condition was binary, so we only have two possible strategies. In many real-world cases the condition is more like a switch statement than an if, and there are several possible strategies.
Second, in our case each API of the Strategy was isolated, with no shared state, and no multi-call relationship. But that’s not at all a requirement. Strategies can have state, and the API’s on them can do entirely different things.
The only thing that matters for the strategy is the question: "are all of these methods consequences of the same decision". If they are, we’re good.
Josh Kerievsky, in his excellent Refactoring To Patterns, shows a nice case, where we use strategy as a kind of sub-type. There are lots of different kinds of loans, but only one kind of Loan object. Internally, it’s "type", Rollover, Fixed, Adjustable, etc., is a strategy.
The LoanStrategy supports lots of different financial calculations for the client, some related, some unrelated. The invariant parts of a loan, both data & algorithm are in the Loan, and the variant parts, both data & algorithm, are in the strategies.
Third, there’s often a performance impact to using Strategy. In our simple case we replace a few hundred ternaries with a virtual function call. The difference in performance should be modest, and I suspect, positive.
The more expensive determining the condition is, the bigger the performance gain. The cheaper determining the condition is, the more likely you’ll be doing something net negative.
Remember, though, we don’t reason about runtime performance, we measure it. If SplitPane performance was a critical path for me, I’d test these alternatives.
Fourth, the consequences of Strategy for productivity are often enormous. Constantly re-evaluating adds a lot of noise to code. That noise really slows a coder down. The computer doesn’t care about this, but the human cares a very great deal about it.
I have found many a bug in code like that of SplitPane, because that noise hides meaning. Further, it is not trivially easy to distinguish between "important" and "unimportant" dimensions. Naming & chunking are critically important to both our productivity and our quality.
Fifth, there are nice testability consequences. Each strategy can be tested separately, not only from its client, but from the other strategies. The corresponding tests are far easier to write and maintain.
Over the years, I have used at one time or another nearly all of the patterns in the GoF book. Often enough, especially in the beginning, I used them where they weren’t a particularly good idea and made things worse rather than better.
Now, in my dotage, I use variants of the Strategy pattern a lot. Like most of the patterns, it has become a way of "seeing" and "speaking" and "thinking" to me. It’s an everyday thing for me to look at repetitively branchy code and ask myself, hmmm, would a strategy be better?
Strategy, at its most abstract, provides "pluggable algorithms" to some client, who can choose whichever algo is right based on situation. My most common use for it: capturing the "consequence in code" of a condition so I can use that consequence all over w/o re-checking.
The GeePaw Podcast
If you love the GeePaw Podcast, show your support with a monthly donation to help keep the content flowing. Support GeePaw Here. You can also show your support by sending in voice messages to be included in the podcasts. These can be questions, comments, etc. Submit Voice Message Here.