The blog for Design Patterns, Linux, HA and Myself!
In this tutorial we’ll be learning about the Strategy Design Pattern. We’ll be creating some classes for Ducks using the Inheritance to pass on the behaviour from parents to the children. While doing so, we’ll be encountering some design challenges, and then we’ll be using the Strategy Design Pattern to fix those issues. In the second part, we’ll be looking into some helpful real world examples created using Java APIs, and then we’ll look into the use case scenarios in which the Strategy Design Pattern can be applied.
We’ll start by creating a set of classes for
abstract
Duck.abstract
duck.abstract
duck.We’ll consider that the technique all the ducks use to swim and quack is same and as they are same, the methods swim()
and quack
will be present in the abstract
duck class, however, the method
, display()
, characterizing the display properties, i.e., will be overridden by all concrete ducks, Red Head or the
Mallard Duck.
Note: Make sure to go through the code comments as well. It’ll help you understand the concept better.
abstract class Duck {
public String quack() {
return "Quack!";
}
public String swim() {
return "Swim!";
}
abstract public String display();
}
The Mallard Duck’s code with just the implementation of the display()
method:
class MallardDuck extends Duck {
public String display() {
return "Looks like a Mallard Duck!";
}
}
The Red Head Duck’s code, like Mallard Duck’s code, with just the implementation of the display()
method:
class RedHeadDuck extends Duck {
public String display() {
return "Red head Duck has a red head!";
}
}
So, what have we achieved here?
We’ve used the power of Inheritance to pass the swim()
and quack()
methods from the abstract duck to all the concrete
ducks. This way all the new ducks will not have to care about the how to’s of these tasks as these will already be
ingrained.
Well, our code looks good until now because we’ll be adding a new feature, FLY
, for our ducks.
From the first glance, there seems to be an obvious solution to it then. Let’s make a fly method inside the abstract duck class and all the concrete ducks will be able to fly.
What’s the another solution? Maybe, we can keep the fly()
method, in the abstract Duck class, abstract and allow all
the concrete ducks to override it by providing their own method to fly.
fly()
method will be same
for both the Mallard duck and the Waterfowl duck and this is code duplication.So, both of the solutions have failed. Let’s think through it.
All the flying behaviours that the ducks use are some standard algorithms. Most of them are not unique to a duck and they are common among them.
So, we find out that the fly behaviour is one which is varying on duck by duck basis. Let’s put this behaviour out then.
We’ll create a set of flying algorithms and the ducks will then use it. This way we’ll separate out the attributes that stays the same from the ones that vary a lot.
The varying attributes will be converted into a set of algorithms and it will be composed into the ducks.
Let’s start coding! We’ll start by creating multiple flying algorithms.
These algorithms or flying techniques will implement a Flybehavior
interface. The FlyBehaviour
is the extracted out
part of the Duck
that kept varying.
// This is the extracted out behaviour.
// Multiple concrete behaviours will be used by the Duck class to
// execute it's performFly method.
// This behaviour is a must have for a duck.
interface FlyBehaviour {
String fly();
}
The FlyWithWings
behaviour:
class FlyWithWings implements FlyBehaviour {
public String fly() {
return "fly with two wings";
}
}
The FlyNoWay
behaviour(for the ducks that do not fly):
class FlyNoWay implements FlyBehaviour {
@Override
public String fly() {
return "cannot fly";
}
}
The changes that we’ve to make for accommodating the FlyBehaviour
inside the Duck
class:
abstract class Duck {
private FlyBehaviour flyBehaviour;
Duck(FlyBehaviour flyBehaviour) {
if (flyBehaviour != null) {
this.flyBehaviour = flyBehaviour;
} else {
throw new UnsupportedOperationException("bad fly behaviour");
}
}
public String quack() {
return "Quack!";
}
public String swim() {
return "Swim!";
}
String performFly() {
return flyBehaviour.fly();
}
abstract public String display();
}
If you notice the performFly()
method you’ll find that the Duck doesn’t care about the type of the FlyBehaviour
. It
just calls the method fly
. This is how we’ve removed the varying part from the duck class. Also, since this behaviour
is being passed in the constructor it had become dynamic. This way the ducks can change their flying algorithms
dynamically.
class MallardDuck extends Duck {
MallardDuck(FlyBehaviour flyBehaviour) {
super(flyBehaviour);
}
@Override
public String display() {
return "Looks like a Mallard Duck!";
}
}
And, this is strategy design pattern.
The definition from the Wikipedia
In computer programming, the strategy pattern (also known as the policy pattern) is a behavioral software design pattern that enables selecting an algorithm at runtime. Instead of implementing a single algorithm directly, code receives run-time instructions as to which in a family of algorithms to use.
As, we’ve a clear picture of the strategy design pattern, let’s look into two examples in which we’ll work with the Java APIs to create real world use cases to better understand this pattern.
In the first example I’ll be using the Comparator
and List
interfaces. I’ll create a list of Teams and then I’ll
create two sorting strategies to sort the teams. The class for the Team is:
class Team {
// stores the number of players
int players;
// stores the number of goals scored by the team
int goalScored;
// stores the number of goals scored against the team
int goalAgainst;
}
It has three fields for its players count, goals that it has scored, and the goals that are scored against it. To
understand the performance of the teams we would like to sort them on those fields. The Team class can implement the
Comparable
interface, and the sorting logic can reside inside the compareTo
method. But there is a problem in that
approach. Different fields will have different sorting logic and based on the season and other factors this logic will
always keep on changing. So, instead of that approach I’ll create multiple comparators based on the requirements.
Here are the two comparators:
class PlayerCountComparator implements Comparator<Team> {
public int compare(Team t1, Team t2) {
return Integer.compare(t1.players, t2.players);
}
}
class GoalDifferenceComparator implements Comparator<Team> {
public int compare(Team t1, Team t2) {
return Integer.compare(t1.goalScored - t1.goalAgainst, t2.goalScored - t2.goalAgainst);
}
}
PlayerCountComparator
sorts on the count of the players whereas GoalDifferenceComparator
sorts on the difference
between the goals scored and goals against.
Let’s create a list of the teams:
Random generator = new Random();
List<Team> teamList = new ArrayList<>(10);
for (int i = 0; i < 10; i++) {
Team team = new Team(generator.nextInt() % 10, generator.nextInt() % 10, generator.nextInt() % 10);
teamList.add(team);
}
Then to sort them we can use the List#sort
method, for example:
teamList.sort(new PlayerCountComparator());
or
teamList.sort(new GoalDifferenceComparator());
Here I can pass any Comparator
to sort the teamList
and due to it the sorting logic is totally decoupled from the
Team
class or the teamList
variable. This is same approach that we took for FlyBehaviour
as well.
Let’s look into one more example to understand it better. Here’s a class for a Server
.
class Server {
public static void writeHostname(Writer w) throws IOException {
String hostname = InetAddress.getLocalHost().getHostName();
w.write(hostname);
w.flush();
}
}
The method writeHostname
uses the writer(w) to write the hostname of the server. For the Server
class it does not
matter where the hostname will be written. All it cares is that it receives a Writer
object to perform the write
operation. This way the destination of the write()
is completely out of picture for the Server
. We can ask it to
write on a File, like:
public static Writer getFileWriter() throws IOException {
File file = new File("all_hostname.txt");
boolean created = file.createNewFile();
if (created) {
return new FileWriter(file);
}
throw new IOException("cannot create a new file");
}
public static void main(String[] args) throws IOException {
Writer fileWriter = getFileWriter();
Server.writeHostname(fileWriter);
}
Or on the Standard Output:
public static Writer getStandardOutput() {
return new OutputStreamWriter(System.out);
}
public static void main(String[] args) throws IOException {
Writer soWriter = getStandardOutput();
Server.writeHostname(soWriter);
}
It remains the same for the Server
as the writing strategy is moved outside of the class.
FileWriter
or the
StandardOutput
writer. Then Strategy Design Pattern provides a way to apply these algorithms.Server
class the writeHostname
method is receiving a Writer
instead of the OutputStreamWriter
or the FileWriter
.I’ve created these tutorials after learning Design Patterns from this book Head First Design Patterns (A Brain Friendly Guide).
Please do add helpful comments about the content so that it can be made better.