22
Strategy

Summaries

Roadmap

Motivation

One of the dominant strategies of object-oriented design is the "open-closed principle".

Software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification. [Meyer, p23]
Figure 21-1 demonstrates how this is routinely achieved - encapsulate interface details in a base class, and bury implementation details in derived classes. Clients can then couple themselves to an interface, and not have to experience the upheaval associated with change: no impact when the number of derived classes changes, and no impact when the implementation of a derived class changes.


Figure 21-1. An "open-closed principle" approach

A generic value of the software community for years has been, "maximize cohesion and minimize coupling". The object-oriented design approach shown in Figure 21-1 is all about minimizing coupling. Since the client is coupled only to an abstraction (i.e. a useful fiction), and not a particular realization of that abstraction, the client could be said to be practicing "abstract coupling" . an object-oriented variant of the more generic exhortation "minimize coupling".

A more popular characterization of this "abstract coupling" principle is ...

Program to an interface, not an implementation.
Clients should prefer the "additional level of indirection" that an interface (or an abstract base class) affords. The interface captures the abstraction (i.e. the "useful fiction") the client wants to exercise, and the implementations of that interface are effectively hidden.

The Strategy design pattern discusses and documents the leverage represented in Figure 21-1. Other terms that are part and parcel of this same concept are: polymorphism, and dynamic binding.

Structure Summary

Structure category:  
inheritance hierarchy
Similar patterns:   Factory Method   Visitor

Before and After

The class Stat below takes an array of integers and can return the min, max, and median values present. Its implementation is inextricably coupled to an embedded bubble sort algorithm. The choice of sort algorithm is not intrinsic to the abstraction of Stat; all that Stat wants is some component capable of sorting an array of integers. If the choice of sort algorithm were configurable, then users of Stat could decide for themselves what kind of "time versus space" trade-offs they require.

Let's abstract the notion of sort and represent that abstraction with an interface. Stat will now "program to an interface" by holding and referencing a pointer to that interface. How the pointer is initialized will be addressed later.

Our bubble sort algorithm can then declare itself to be a concrete implementation of the abstract interface. With the client coupled only to the SortImpl interface, our design is now "open for extension". Let's demonstrate that new-found leverage by adding a shell sort algorithm. Rather than hard-wire the choice of sort algorithm in the Stat class, we can choose a default implementation in the constructor, and allow the client of Stat to change it at will. main() is now changed to exercise the new upGrade() and downGrade() methods. The output appears at the end. When we defined our interface in a base class, buried implementation in derived classes, and coupled the client to the abstraction represented by the interface, we exercised the object-oriented feature called polymorphism. My favorite definition of polymorphism is
one interface, many shapes (or implementations)
It is also referred to as run-time (or dynamic) polymorphism, because the code generated by the compiler has sufficient indirection built-in so that the choice of sort algorithm can be decided at run-time (instead of having to be declared at compile-time).

An alternate implementation approach for the Strategy pattern uses C++ templates. Templates have been described as compile-time (or static) polymorphism. Consider the following code example. The only expectations the OutsideClass places on the delegate object are: must have a do_one() method, and must have a do_two() method.

C++ templates allow "data types" referenced by a class "to be supplied" by the client of that class. Here is the syntax for a class template. A C++ compiler that supports templates is capable of parsing this source code, identifying the do_one() and do_two() requirements, and allowing any class that satisfies these requirements to be specified by the client. A new class and the client code appear below. C++ templates are useful if the type of referenced objects are known at compile-time, and those objects do not need to be swapped with other objects from the same inheritance hierarchy at any point during run-time. Let's try a template out on our previous sort example. When a template is used, there is no need to create a "lowest common denominator base class" that makes all derived classes interchangeable. In this case, any class that provides a method with the signature void sort(int[],int) can be supplied to the Stat class. The Strategy design pattern is about making alternate implementations of an algorithm interchangeable, and coupling the client of that algorithm to an interface only. In C++ the designer has two approaches to consider: dynamic polymorphism using a virtual function in an inheritance hierarchy, or static polymorphism using a class template.

Non-Software Example

"A Strategy defines a set of algorithms that can be used interchangeably. Modes of transportation to an airport is an example of a Strategy. Several options exist such as driving one's own car, taking a taxi, an airport shuttle, a city bus, or a limousine service. For some airports, subways and helicopters are also available as a mode of transportation to the airport. Any of these modes of transportation will get a traveler to the airport, and they can be used interchangeably. The traveler must chose the Strategy based on tradeoffs between cost, convenience, and time." [Duell, p54]


Figure 21-2. Choice of "go to airport" algorithm

Structure


Figure 21-3. Decouple client from algorithm implementation

The Interface entity in Figure 21-3 could represent either an abstract base class, or the method signature expectations by the client. In the former case, the inheritance hierarchy represents dynamic polymorphism. In the latter case, the Interface entity represents template code in the client and the inheritance hierarchy represents static polymorphism.

Behavior


Figure 21-4. Client talks to Interface, Implementation actually responds

Figure 21-4 is faux UML. The intent is to demonstrate that the client is coupled only to the Interface, but the actual recipient of any request is whichever implementation class has been instantiated.

Summary

  1. Identify an algorithm (i.e. a behavior) that the client would prefer to access through a "flex point".
  2. Specify the signature for that algorithm in an interface.
  3. Bury the alternative implementation details in derived classes.
  4. Clients of the algorithm couple themselves to the interface.

Example 1

Formatting dates is a fairly popular algorithm that has been encapsulated many times in the past. Let's use this domain as a vehicle for discussing the Strategy pattern. It would be nice if . we had a polymorphic capability of producing the following kinds of date formats. Consider a BillingSystem that needs to format dates - and - wants the format used to be configurable. The code below simulates the "client" of a DateFormatStrategy abstraction. Figure 21-5 captures the relationships and decoupling we intend to implement.


Figure 21-5. Encapsulating the Date formatting algorithm

A possible suite of DateFormatStrategy implementations follows. Given the richness of the Java standard library, it could be argued that there is not sufficient justification to create a new Strategy inheritance hierarchy for so little leverage. The significance of this example though, is not the "letter of the implementation", it is the "spirit of the pattern".

These Strategy implementations can be tested polymorphically with the following client code. Let's go back to the original BillingSystem implementation. The if...else construct that tests whether the formatter object has been initialized is problematic. Right now that test only occurs in this one place. But - as the application evolves and grows, anybody that uses the formatter object will have to perform this same test. It would be nice if this brute-force, state-oriented, C-style of "test and branch" programming could be encapsulated within the DateFormatStrategy "extra level of indirection" that we already have in place.

There is a non-gang-of-four pattern that addresses this kind of opportunity - the Null Object Pattern.

A Null Object simplifies the client by relieving it of having to write special testing code to handle "lack of initialization" situations. It encapsulates the implementation decisions of how to do nothing and hides those details from the client. [Woolf, pxxx]
The idea is to "promote to full object status" whatever it means to be un-initialized. In this case, the now.toString() statement can become a class of its own, and the formatter attribute can be assigned a default value of new NullStrategy. Notice that the print_statement() method can now delegate to the formatter object without first checking to see if it has been initialized. Removing extraneous control logic is always a good thing.

Example 2

Consider Java's TreeSet class. It guarantees that all objects added will be maintained in ascending order. In the example below, the TreeSet object uses the compareTo() method is the containee class Struct to keep the elements sorted. The relationships are portrayed in Figure 21-6. Note that the compare functionality that makes the sorting possible is fixed in the containee class. The user of TreeSet is not able to configure any alternate ordering.


Figure 21-6. Compare functionality for sort in containee class

It would be nice if the "compare" algorithm were decoupled from the Struct class, and implemented as a Strategy. That would allow the user of the TreeSet class to choose from a menu of Strategy objects and customize the ordering of contained objects. Each CompareXxx class keys off of a different attribute of the Struct class.

We have effectively refactored the "compare" functionality to full object status. It has migrated from the Struct class to a Strategy inheritance hierarchy. Figure 21-7 demonstrates this new "degree of freedom". TreeSet still contains Struct objects. But it now delegates "compare" functionality to the Strategy object with which it is configured.


Figure 21-7. "compare" is now a Strategy algoritihm

Example 3

The C++ standard library sort() algorithm is a subtle analogy for the static polymorphism version of the Strategy design pattern. sort() is case sensitive by default. The results shown below demonstrate that a case insensitive sort would be nice. Fortunately, the sort() algorithm will accept any "compare" algorithm that supports the correct "signature" - the function must return a bool, and accept two objects by const reference of the same type as the first two arguments to sort(). The implementation of no_case_sort_function() came from Stroustrup, p467. A C++ alternative to the C function pointer is a "functor" or "function object". A functor is an instance of a class that defines the function call operator (aka, the application or parentheses operator). This feature effectively promotes to full object status the notion of a function invocation. The benefits of this "extra level of indirection" include: the traditional object-oriented leverage of co-locating state and behavior, and improved performance as a consequence of the inlining of code that will likely result. Figure 21-8 presents an abstract characterization of these implementation alternatives. The letter of the UML law has been violated, but the spirit has been preserved. The sort() function has decoupled itself from the algorithm" of "compare". It is happy to delegate all compare functionality to any entity that can fulfill the TBD function signature. The user of sort() can specify at compile-time not only the compare algorithm, but also the choice of language feature.


Figure 21-8. sort() is coupled to a signature, not an implementation

Heuristics

"Strategy is like Template Method except in its granularity." [CoplienMar96, p88]

"State is like Strategy except in its intent." [CoplienMar96, p88]

"Strategy lets you change the guts of an object. Decorator lets you change the skin." [Gamma, p184]

"State, Strategy, Bridge (and to some degree Adapter) have similar solution structures. They all share elements of the 'handle/body' idiom. They differ in intent - that is, they solve different problems." [Coplien, p58]

"Strategy has 2 different implementations, the first is similar to State. The difference is in binding times (Strategy is a bind-once pattern, whereas State is more dynamic)." [CoplienMar96, p88]

"Strategy objects often make good Flyweights." [Gamma, p323]

In C++, "policy classes" are "classes that encode an action that is largely orthogonal with respect to any other [flex point]." [Alexandrescu, p258] They ... capture and encapsulate reusable behavior that would otherwise proliferate. "Policies are reminiscent of the Strategy design pattern with the twist that policies are compile-time bound." [Alexandrescu, p8] The hardest part of creating policy-based class design is to correctly decompose the functionality of a class. The rule of thumb is ... anything that can be done in more than one way should be migrated from the class [and isolated as] a policy." [Alexandrescu, p19]