The Big Car Showroom and the Strategy Design Pattern
Let’s say we secured the positions of software developer interns at a supermarket. Our task is to develop an application for the market that lists all the cars on sale.
We started with the basics and we let all the car types inherit from the class Car
. The listManufacturer
method lists the details and brand of the car manufacturer and it varies from subclass to subclass in our case, which is why it is kept abstract and class Car
is an abstract class. Methods run and carryPassengers
are ordinary and generic methods as shown below.
void run() {
System.out.println("Run from point A to point B");
}void carryPassengers() {
System.out.println("Carry passengers and their luggages");
}
Everything looks perfect and the `Inheritance` seems to be solving all of our problems here like the perfect piece of the puzzle.
But the dummy car or toy car does not have carryPassengers
feature and it should not inherit the carryPassengers
method. In such scenarios, implement the following object oriented design principle.
Identify aspects of your applications that vary (carryPassengers) and separate them from what remains same.
Abstract methods listManufacturer
and run remain the same for subclasses as far as inheritance is concerned. While carryPassengers
method does not. Let’s encapsulate what changes so it does not affect rest of the code.
We wrote an interface Carriable which has abstract method carryPassengers
. And only those cars (classes) implement the interface which can actually carry passengers. Bingo! We successfully segregated the varying part in implementation. Let’s focus on code maintenance.
Here, carryPassengers()
is separately and individually implemented in each Carriable class. (As of now, let’s ignore other methods for simplicity.)
class TeslaCar extends Car implements Carriable {
void carryPassengers() {
System.out.println("Carry passengers and their luggages");
}
}class HondaCar extends Car implements Carriable {
void carryPassengers() {
System.out.println("Carry passengers and their luggages");
}
}
Now imagine, we need to change the behaviour of method carryPassengers
and carryPassengers
consists of 100 lines of code and there are 100 classes of type Carriable
.
To change the behaviour of method carryPassengers
, we will need to touch 100 classes of type Carriable individually and separately. Well this does nothing but invites bugs into code and makes code maintenance difficult. Touching each subclass of type Carriable renders the code design inflexible.
The concrete implementation of carryPassengers
from Carriable
interface has been given in class Carry
and CarryNot
class Carry implements Carriable {
public void carryPassengers() {
System.out.println("Carry passengers and their luggages");
}
}Class CarryNot implements Carriable {
public void carryPassengers() {
System.out.println("Cannot carry passengers and their luggages");
}
}
On the other hand, abstract class Car
has instance variable carriable to look at concrete class Carry
or CarryNot
and consequently to look at varying implementation of method carryPassengers
encapsulated in classes Carry and CarryNot
.
Each subclass of class Car
will be looking at either Carry or CarryNot via inherited instance variable carriable of type Carriable.
The abstract class Car has method setCarriable(Carriable carriable)
to decide which type of Carriable
to look at, whether to look at class Carry
or CarryNot
.
void setCarriable(Carriable carriable) {
this.carriable = carriable;
}
Each subclass of class Car
will also inherit carryPassengers()
method from parent abstract class Car
. The method carryPassengers
does nothing but calls carryPassengers()
method of already set carriable.
void carryPassengers() {
carriable.carryPassengers();
}
Now if we need to change behaviour of carry
method, we need to touch only one subclass Carry
. This approach is far more flexible than the previous approach.
Strategy design pattern defines a family of algorithms (in our case, the varying code of carryPassengers
) encapsulates each one (in our case, class Carry
and CarryNot
) and makes them interchangable (in our case, Carriable carriable variable could look to classes Carry
and CarryNot
). It lets algorithms vary independently from the client that uses it. (In our case, we could modify carryPassengers
behaviour without touching subclasses of class Car
).