Strategy Pattern
Goals
- Learn about design patterns in general.
- Understand the strategy pattern.
- Find out how the
Comparator<T>
strategy works. - Use
Comparator<T>
in data structures.
Concepts
- behavioral patterns
- composite pattern
- creational patterns
- design pattern
- factory method pattern
Gang of Four
(GoF)- iterator pattern
- natural ordering
- strategy pattern
- structural patterns
Library
java.lang.Integer.compare(int, int)
java.lang.Comparable<T>
java.lang.Comparable.compareTo(T)
java.util.Arrays
java.util.Arrays.sort(T[], Comparator<? super T>)
java.util.Comparator<T>
java.util.Comparator.naturalOrder()
java.util.Comparator.reversed()
java.util.Comparator.thenComparing(Comparator<? super T>)
Lesson
Design Patterns
In 1994 Erich Gamma, Richard Helm, Ralph Johnson, and John Vissides published a book that was to become central to object-oriented architecture. These four authors, subsequently referred to as the Gang of Four
(GoF), had noticed certain repeated structural elements in object-oriented programming—ways that good developers use classes to solve certain common tasks. They named these similar approaches design patterns, and in their book Design Patterns: Elements of Reusable Object-Oriented Software they named and explained 23 different design patterns categorized as either creational, structural, and behavioral. The names they gave to the design patterns in each of these categories are now used across the industry.
Design patterns are not rules. You do not have to use a design pattern, although the better a developer you become you may accidentally use one or invent
one without knowing it. Likewise if you use a design pattern you do not have to hold to all the details in the description given by the Gang of Four. Design patterns are like guideposts from experts who have had to deal with similar problems; if nothing else they provide a common language so that you can describe the approach you are taking to solve a problem.
In the preceding lessons you have already been introduced to several design patterns that were described in the Design Patterns book. They have become so ingrained in Java programming that you may come to think of them as given, as permanent structures in the Java landscape, as indeed they now are. In this lesson you will also learn a new design pattern, the strategy pattern.
Factory Method Pattern
When learning about generics, you created a static factory method for your linked list implementation. The factory method pattern is one of the creational design patterns, and has more uses than merely serving as a convenience for replacing the constructor. Later in this lesson you'll learn how to create factories to produce objects of different types, configured using the strategy pattern.
Composite Pattern
Throughout these lessons you have learned how to compose one class out of one or more other classes. With graph classes such as linked lists and trees you have learned how to compose classes out of an indefinite number of other node classes strung together. These are all examples of the composite pattern, one of the structural design patterns.
Iterator Pattern
Most recently you learned how to extract out an interface for iterating some underlying data structure, allowing the interface implementation to worry about how to navigate the objects contained in the structure. This approach to abstract is the iterator pattern, which is another of the structural design patterns.
Strategy Pattern
The behavioral design patterns help specify how programs behave, as you might have guessed from their name. You might at first naturally tend to implement behavior directly into your classes. Consider that you are writing the software to control a boat with the latest features. The boat needs to deal with the situation in which the helmsman (the person steering the boat) falls overboard. You might implement the emergency procedures so that the boat's speed is cut in half so that other can better react to the situation:
The marketing department may determine that cutting speed to 50% is still too dangerous—instead you should cut speed to 10% of the original speed. You'll have to rewrite your Boat
class:
After the Boat
has been on the market for a while, the marketing department decides to release another variation of the boat that will set a constant speed of 5 knots. That way the emergency speed won't depend on the speed at the time the helmsman went overboard. Marketing wants to sell both versions of the boat simultaneously, calling one the Model 1 and the other the Model 2. So what do you do? You could refactor the current Boat
class into an AbstractBoat
class with an abstract onHelmsmanOverboard()
method. Then you could create both Model1Boat
and a Model2Boat
subclasses, each with a different onHelmsmanOverboard()
implementation.
But by this time you've become wise to the ways of the marketing department, so to keep from rewriting your Boat
class over and over in the future, you create a simple interface that will determine the new speed based upon the old speed in emergency conditions. You name this interface the HelmsmanOverboardEmergencyProcedure
, and it will return the new speed of the boat in emergency conditions
You retrofit the Boat
class to delegate to this new emergency procedure interface to know what to do when the helmsman goes overboard:
When the helmsman goes overboard, you simply give HelmsmanOverboardEmergencyProcedure.getNewSpeed(…)
your current speed, and it gives you back the new speed you should take in these emergency conditions. Now you can write an implementation for HelmsmanOverboardEmergencyProcedure
for each model of the boat, and use the correct procedure implementation when you need it.
Encapsulating some sort of logic into a second class or interface, rather than implementing the logic directly into the first class, is referred to as the strategy pattern. The second class provides a strategy
for performing some task. Rather than rewriting the original class, one need merely provide a different strategy implementation if different functionality is desired.
Parameterized Strategies
A strategy implementation is a class and can require parameters in the constructor as with any class. We could make a general strategy for setting the speed to some constant value, requiring that a developer provide this constant speed in the constructor. A developer could then create an emergency procedure of 5 knots (equivalent to the FiveKnotsEmergencyProcecure
above) or of any value.
Strategy Constants
If there are strategies you use all the time, there is no point in creating multiple duplicate instances of them. We could create constant instances of these strategies (as long as we make the classes immutable) so that they can be reused. Rather than remembering which strategies we need to pass to which constructors to produce which boat types, we could use a variation of the factory pattern. The BoatFactory
class has methods for calling the Boat
constructor and passing the appropriate strategy. The caller doesn't need to keep track of which strategies to use, and the Boat
doesn't have to know which strategies exist. The BoatFactory
therefore serves as a layer of indirection that makes creating preconfigured Boat
instances more flexible.
These particular strategy implementations aren't complicated; you might decide just to create them in place using anonymous classes.
Interactive Strategies
The strategies we have been looking at so far have been little more than a set of configuration values, although they do illustrate that a strategy does allow more complex calculations that a simple list of values. You can also give your strategy even more control, based upon your needs. You could pass the strategy a reference to the Boat
itself, essentially letting the strategy take control. At the same time we could make this a general strategy for all emergency procedures.
This approach would still allow the strategy to reduce the speed, if that is what is desired:
But it also allows more involved actions, such as turning the boat around and stopping:
Comparator<T>
The standard Java libraries provide various classes and interfaces that can function as strategies. One simple yet useful strategy is the java.util.Comparator<T>
interface. It allows two objects to be compared for order. Its main method, Comparator.compare(…)
, compares two objects and returns an int
indicating which object is greater than the other. The trick is that the actual magnitude of the returned value is not important; what is important is the sign. If a negative value is returned, the first object is less than the second. If a positive value is returned, the second object is greater than the second. If zero is returned, the objects are considered equal.
Comparator.compare(object1, object2)
Return Value | Meaning |
---|---|
< 0 | object1 < object2 |
0 | object1.equals(object2) |
> 0 | object1 > object2 |
For example, we could compare two Point
classes based upon their X coordinates:
Arrays.sort(…)
To see how we can use a Comparator<T>
, consider the Java utility class java.util.Arrays
for working working with arrays. One of its static methods is java.util.Arrays.sort(T[], Comparator<? super T>)
, which allows us to sort an array of objects based upon a given Comparator<T>
strategy implementation. We can use the PointXComparator
above to sort the points based upon their horizontal position.
Comparator<T>
Utilities
The java.util.Comparator<T>
interface also comes with a wealth of utility methods and default implementations. Here are a few useful ones; you are encouraged to see the API documentation to discover even more.
naturalOrder()
- Constant method that returns a read-made comparator of the correct type for comparing items based upon their natural ordering, which is the way the JVM naturally sorts value such as primitive wrappers, or the ordering imposed by
Comparable.compareTo(T)
for those objects that implementjava.lang.Comparable<T>
. reversed()
- Default method that returns a new comparator that will produce values resulting in a reverse ordering of the original comparator.
thenComparing(Comparator<? super T>)
- Default method allowing you to
chain
comparators. The new comparator returned will use your provided comparator as a secondary comparator if the original comparator considers two elements equal.
Review
Gotchas
- Don't return the difference of two integers inside
Comparator.compare(…)
, as this can result in an integer overflow.
In the Real World
- If possible make your strategies immutable and able to work with various targets rather than being tied to one object.
Self Evaluation
- What is a design pattern? Name three you have already studied.
- What is the benefit of the strategy pattern?
- Why might you use an anonymous class to implement a strategy?
- What does it mean for a comparator to be
inconsistent with equals
?
Task
When you wrote your BookBinarySearchTree
class, you sorted the books based upon their publication date. You couldn't make the BST general—that is, supporting any type of object—because you relied on the publication date provided by the Book
class for sorting. You had no way of knowing how to sort general objects. But now you've learned about the strategy pattern and how the java.util.Comparator<T>
interface allows you to encapsulate sorting logic for any object in a separate comparator strategy implementation. So retrofit your BookBinarySearchTree
to work with any type, providing a Comparator<T>
to indicate how objects should be compared.
- Create a
BinarySearchTree<T>
interface (presented below) in thedatastruct
project. - Rename
PublicationBinarySearchTree
toBinarySearchTreeImpl<T>
, implementingBinarySearchTree<T>
, and move it to thedatastruct
project. - Retrofit
BinarySearchTreeImpl<T>
to work with any generic typeT
instead of only subclasses ofPublication
.BinarySearchTreeImpl<T>
's constructor will need to take and store aComparator<T>
in order for the BST to know how to sort the objects.
- Create a new set of unit tests in the
datastruct
project for your new BST implementation.- Test the BST with some simple-to-understand class such as
Integer
.
- Test the BST with some simple-to-understand class such as
- Leave your existing BST unit tests in the
booker
project.- In the unit test class for the book BST, create an implementation of
Comparator<Book>
that compares based upon publication date, just like the originalBookBinarySearchTree
did. - Supply this
Comparator<Book>
implementation to all the BST instances in your existing unit tests for the originalBookBinarySearchTree
. - Remove any tests that depended on accesing the book BST nodes.
- All the existing tests should continue to work with no other modifications.
- In the unit test class for the book BST, create an implementation of
/**
* A binary search tree.
* @param <T> The type of object contained in the BST.
*/
public interface BinarySearchTree<T> {
/**
* Adds an object to the BST.
* The object will be sorted in the tree based upon the BST implementation.
* If an object is already in the tree with the same comparison characteristics,
* no object is added.
* @param object The object to add.
*/
void add(@NonNull T object);
/**
* Determines if an equivalent object is contained in the tree.
* @param object The object to search for based upon its comparison characteristics and the BST implementation.
* @return true if an object is contained in the tree that compares equally with the given object.
*/
boolean contains(@Nonnull T object);
/**
* Prints all objects in the tree using depth-first in-order traversal.
* This implementation uses the {@link Object#toString()} method of each object in the tree.
*/
void printAll();
}
See Also
References
Acknowledgments
Comparator<T>
code snippet extracted from source code Copyright © 1997, 2014, Oracle and/or its affiliates.- Some symbols are from Font Awesome by Dave Gandy.