Lecture 14: The Strategy and Decorator Patterns

The strategy and decorator patterns are very similar idioms of
using delegation to refactor aspects of a class’s behavior into separate
classes, for easier enhancement or modification, and tighter cohesiveness. The
strategy pattern is somewhat more intuitive to motivate with real-world
examples, so we’ll start there.

1The strategy pattern

Let’s consider a straightforward game for which the technique for winning is
easily, thoroughly mastered: TicTacToe. How might we play this game? Here are
some ideas:

Play in an open square

Play to an open corner

Block the opponent from winning

Place a third piece in a row to win

All but the first of these aren’t complete strategies. If the corners are all
full, for instance, then the second approach doesn’t have any recommendation to
make. So perhaps more accurately, our complete strategies might be:

Play in an open square

Play to an open corner, or else play in an open square

Block the opponent from winning if they’re about to, otherwise play
to an open corner, otherwise just play to an open square

Place a third piece in a row to win, or else block the opponent if
they’re about to win, or else play in an open corner, or else just
play to an open square

It’s probably intuitively clear to you, as an expert human player of TicTacToe,
that the final strategy above is pretty good.1It’s not perfect — suppose
two players were following this same strategy. Would the game end in a win,
loss, or tie for the first player? How could you fix any problems you might
encounter? How might we implement these strategies?

1.1A strategy is just a function object

Suppose in our program we had a TicTacToeModel containing the board
state, and a Piece enumeration naming X and O. Our program
also contains two Player objects (one for each Piece), which may
call the model’s playMove(Piece player, int row, int col) method to make
a move. How should the Players choose where to move? What information
would be needed to make an informed choice? We’d need to know the state of the
board, and also know which player we’re trying to choose a move for:

How might we implement the second strategy? We could implement the entirety of
the logic (choose corners or else choose random), but part of that logic has
been implemented already... Let’s implement just the new part, first:

This strategy looks for an open corner, or else gives up. So how might we
combine the pieces we have so far to produce the composite strategy?

1.2Strategies can be composed

The any-open-square strategy and the any-open-corner strategy are both
functions that take in a board state and return a coordinate to move. We can
easily, and generally, combine the two of them into a higher-order
strategy that first tries one and then, if it fails, tries the other:

Now, our full second strategy is simply
new TryTwo(new AnyOpenCorner(), new AnyOpenSquare()). We can continue
implementing the other strategy components (blocking an opponent, or going for
an immediate win), and combining them with TryTwo. (These other
components need the forWhom argument — try implementing them
yourself.)

More broadly, strategies can be composed in lots of different ways, analogous
to mapping, and-map, or-map, or other higher-order combinations. We can
generalize from TryTwo to TryMany (that takes a list of
strategies); we can generalize to randomly selecting among several strategies,
or more sophisticated choices among several strategies, etc.

1.3Strategies can be dynamically selected

Suppose we wanted to build a TicTacToe game for players of varying ability, and
wanted “easy”, “medium”, and “hard” difficulty levels. We could easily
mix and match compositions among the four simple strategies above, and
dynamically assign some such object at runtime:

Rather than hardcoding the difficulty level of the game, we have refactored it
out and made it easily configurable.

We can also define an AskUser strategy...and now we have something the
controller can use to interact with a human player, in the same framework as
these other strategy choices.

1.4Strategies make for easy testing

If we supply two strategies to our controller and say “play the game with two
players using these respective strategies”, what could happen? If both
strategies are fully automatic, then we’ve built a great test harness to run
through the game automatically! Combining this with the Appendable
output we’ve seen in earlier lectures, and we have a straightforward means to
simulate a playthrough of the game. Moreover, testing different strategies
against each other lets us not only test whether they work properly, but also
how well they do at playing the game.

2The decorator pattern

The decorator pattern is similar to the strategy pattern in terms of how
objects delegate from one to another, but the purpose is different. The
canonical example of decorators is a UI widget library, such as Swing. We
might have a basic JPanel class that just describes a rectangle of
on-screen content. We might have a JScrollPanel class, which is a
subclass of JPanel, that visually wraps around some other JPanel
and adds scrollbars and the ability to scroll the view in one or more
directions. Or a JSplitPanel, which surrounds two panels and produces a
split-screen effect. Or a JGridPanel, or others... Each of these
classes obeys a fairly sophisticated interface (all the methods of the base
JPanel class), and then does two things: it provides some new
functionality of its own, and delegates the rest of the functionality to inner
panels. Visually, each surrounding panel decorates the inner ones by
adding borders, scrollbars, splitters, etc. Just as with the strategy pattern,
each class is responsible for one fragment of functionality: the complete
functionality comes about by composing several decorators together around some
base object. (Unlike strategies, where there can often be useful strategies
that do not need to delegate at all; decorators are all about the delegation.)

3Strategies, Decorators and Inheritance

When should you use a strategy pattern or a decorator pattern, instead of just
using inheritance to customize behavior? After all, don’t subclasses
specialize the behavior of their superclasses? For example, could we produce
the same effect by having our User class just have an abstract method
chooseMove, and then creating subclasses that define various
possible implementations for that method?

Yes...but only sometimes. The power of the strategy and decorator patterns
comes from the dynamism inherent in ability to delegate from one simple
strategy to the next, or from one decorator to its contained content. Rather
than being fixed at compile time, we can use higher-order strategies to
mix-and-match strategy pieces, or decorate panels into elaborate UIs, without
having to hard-code those choices in advance. Additionally, the strategy or
decorator classes are appealingly tiny, self-contained, and easy to read: they
have very high cohesion, because they’re built to do exactly one thing and
nothing else.

1It’s not perfect — suppose
two players were following this same strategy. Would the game end in a win,
loss, or tie for the first player? How could you fix any problems you might
encounter?