Design patterns are, ultimately, nothing more than the result of applying a few object oriented principles which you should try to follow.
We will be learning those principles, but it is also good to study some of the patterns that arise from their use so that we don’t have to reinvent the wheel.
Learning established patterns allow you to quickly get to a solution, and they also provide you and other developers with a shared vocabulary that can be used to discuss your code.
During the semester, we will also be discussing how design patterns are actually kind of awful and may be seen as a “least awful” solution to problems in many cases.
The strategy pattern defines a family of algorithms, encapsulates each, and makes them interchangeable. A strategy lets the algorithm vary independent from clients that use it
Before we take that definition apart, let’s take a look at the object oriented principles that lead to this pattern:
Modifying code is usually pretty dangerous, because it can introduce regressions in your code.
If we do not properly encapsulate our code, then simple changes can have far reaching impact.
Accordingly, we would prefer to identify behavior that may vary in our code and isolate it from code that does not vary.
If we do this successfully, then the non varying code can be written, tested, and left alone forever.
…assuming our testing was sufficient
For instance, let’s assume we have the following code
public class Duck{
private final int id;
public Duck(final int id{
this.id = id;
}
public String fly(){
// Let's imagine this is a more complex computation
return "I'm flying";
}
public int getId(){ return id; }
}
In this example, we can be relatively sure that the id related code will not change. However, it is not unlikely that different ducks might fly differently.
So, we might do this instead
public class Duck{
private final int id;
private final FlyBehavior flyer = new FlyBehavior();
public Duck(final int id){
this.id = id;
}
public String fly(){ return flyer.fly(); }
public int getId(){ return id; }
}
public class FlyBehavior{
public String fly(){
return "I'm flying";
}
}
Now, if the flying behavior needs to change, we still never have to handle the Duck class again (almost…).
We still have a slight problem. Our Duck can only store the class FlyBehavior, so the result isn’t that flexible.
How can we support different flying behaviors?
If we write code which uses specific, concrete types, we are stuck with those types and have to manually modify our code, duplicate code and do other awful things to use other types.
If, instead, we program to more abstract interfaces, we can modify the behavior of our code, without modifying the code itself.
Note that, in this context, interface refers to a conceptual interface which can be coded in the form of
public class Duck{
private final int id;
private final FlyBehavior flyer;
public Duck(final int id, final FlyBehavior flyer){
this.id = id;
this.flyer = flyer;
}
public String fly(){ return flyer.fly(); }
public int getId(){ return id; }
}
Now, we can create various subclasses for FlyBehavior and swap them in and out to customize how ducks fly.
Even cooler, we can do this at runtime!
When we want to extend the behavior of our code, we typically have two ways to do it. We can either use inheritance or composition.
Using inheritance, A class A can extend another class B and override or add methods to obtain the desired behavior.
We often say that such an instance of A “is a” B.
We could instead compose objects and include a field of type C inside of a class B and the use that to change the behavior of the class B.
In this case, we would say that B “has a” C
We will learn that it is often valuable to write software so that different components are “decoupled” from each other. This is almost always easier when using composition.
At this point, we have reached a nice clean solution which is, in name, the strategy pattern.
As you see, we were able to reach this point only using basic principles, but it would have been easier to just jump straight to this design!
You should consider using the strategy pattern when:
Some drawbacks of the strategy pattern include:
We know that an interface that contains a single method can be replaced with a lambda expression.
This means we don’t have to define a special interface for the strategy: we just need to specify a general function interface as a parameter.
Additionally, we are passing strategies into constructors, but we could simply pass them into the methods where they are needed.
Storing them in a field is just a convenience.
At this point, we have essentially reached the basic concept of higher ordered functions
A higher ordered function is a function which takes another function as a parameter
If the strategy pattern is basically just a complicated implementation of higher ordered functions, then what is the point of jumping through extra hoops?
As Java incorporates more functional concepts, design patterns like this start to become much less interesting.