Let’s suppose you are creating a duck simulator. This simulator lets you create different types of ducks e.g Mallard Duck, Redhead Duck, etc.
One way to model this is to use inheritance. You can have Duck
base class, and other ducks can inherit the common behaviour from the base class
class Duck {
display() {
// implementation
}
quack() {
// implementation
}
swim() {
// implementation
}
}
class MallardDuck extends Duck {
display() {
// displays mallard duck
}
// inherits the rest of behaviours
}
class RubberDuck extends Duck {
display() {
// displays rubber duck
}
// inherits the rest of behaviours
}
So far it looks right? Let’s suppose a request comes in which requires us to add RubberDuck
to the simulator. Taking advantage of inheritance you extend the RubberDuck
from Duck
base class.
class RubberDuck extends Duck {
display() {
// display the rubber duck
}
}
But there is a problem. A rubber duck can not quack instead it squeaks. We can overcome this by overriding the quack
method to run squeak
class RubberDuck extends Duck {
display() {
// display the rubber duck
}
quack() {
this.squeak();
}
squeak() {
// squeak
}
}
So far the inheritance is holding. Now let’s suppose there comes a feature request which is to make all the ducks fly. You can implement this by adding a fly method to Duck
base class but this presents a new problem as it also makes the RubberDuck
flyable which is not true. One way would be to override the fly behavior in the RubberDuck
so calling fly
method on RubberDuck
does nothing.
Now let’s add another type of duck to our simulator called DecoyDuck
.
class DecoyDuck extends Duck {
display() {
// display the decoy duck
}
quack() {
// do nothing
}
fly() {
// do nothing
}
}
As DecoyDuck
can neither fly or quack we have overridden its fly
and quack
method but is this the best we can do?
As we are overriding base class behaviours in child class we are not getting the full benefits of the inheritance.
Maintaining Duck
hierarchy is causing is more harm than good. It is making code messy and our code reuse is limited as we override the behaviour with no-op for some cases.
The Duck
class is not giving us the whole picture, we have to open individual concrete classes to get the whole picture and one last thing, when we added a fly
method to the base class it resulted in unintended consequences in child class which we then fix by overriding the behaviour in child classes.
Let’s see if we can use the interface to make our design more flexible. We will create two interfaces called Quackable
and Flyable
. Classes can implement this behavior if they need the functionality
interface Flyable {
fly() {}
}
interface Quackable {
quack() {}
}
class Duck {
display() {
// implementation
}
swim() {
// implementation
}
}
class MallardDuck extends Duck implements Flyable, Quackable {
display() {
// displays mallard duck
}
swim() {
// implement swim behaviour
}
quack() {
// implements quack behaviour
}
}
class RubberDuck extends Duck implements Quackable {
display() {
// displays mallard duck
}
quack() {
// call squeak
}
}
class DecoyDuck extends Duck {
display() {
// displays mallard duck
}
quack() {
// call squeak
}
}
This has improved the flexibility of our design but at the same time, it has reduced our code reuse if there is a new type of Duck
it will need to implement the interface and provide the behaviour of the implementation. It has also made the code difficult to maintain. If we have to make some modifications in behaviour we will have to open all the classes and change the behaviour.
One of the object-oriented principle is to encapsulate what varies. This principle says that if some aspect of code is changing you need to pull that out and encapsulate it. Then later on you can simply change the encapsulate code instead of making changes all over the code.
Let’s see how it applies to our duck simulator. In our case, the quacking
and flying
behavior is being changed whenever we add a new duck. The only behavior which is consistent across all the ducks is swim
So what we will do is create two interfaces called QuackBehaviour
and FlyBehaviour
and instead of implementing them directly in our MallardDuck
or RedheadDuck
classes, we will instead implement them in new classes. Lets see this in code:
interface QuackBehaviour {
quack();
}
interface FlyBehaviour {
fly();
}
class Quack implements QuackBehaviour{
quack() {
// quack implementation
}
}
class Squeak implements QuackBehaviour{
quack() {
// squeak implementation
}
}
class Mute implements QuackBehaviour{
quack() {
// mute implementation
}
}
class FlyWithWings implements FlyBehaviour {
fly() {
// fly with wings
}
}
class DoNotFly implements FlyBehaviour {
fly() {
// do not fly
}
}
Now the next step is to add two properties to the Duck
class along with its setter.
flyBehaviour: FlyBehaviour;
quackBehaviour: QuackBehaviour;
setFlyBehaviour(flyBehaviour) {
this.flyBehaviour = flyBehaviour;
}
setQuackBehaviour(quackBehaviour) {
this.quackBehaviour = quackBehaviour;
}
Here is Duck
class with all the new changes:
class Duck {
flyBehaviour: FlyBehaviour;
quackBehaviour: QuackBehaviour;
setFlyBehaviour(flyBehaviour) {
this.flyBehaviour = flyBehaviour;
}
setQuackBehaviour(quackBehaviour) {
this.quackBehaviour = quackBehaviour;
}
fly() {
this.flyBehaviour.fly();
}
quack() {
this.quackBehaviour.quack();
}
abstract display();
swim() {
// swim behaviour here, as it is common among all the ducks
}
}
Now lets modify our concrete duck classes to use this new Duck
parent class.
class MallardDuck extends Duck {
constructor() {
const quackBehaviour = new Quack();
const flyBehaviour = new FlyWithWings();
this.setFlyBehaviour(flyBehaviour);
this.setQuackBehaviour(quackBehaviour);
}
display() {
// displays mallard duck
}
}
class RubberDuck extends Duck implements Quackable {
constructor() {
const squeakBehaviour = new Squeak();
const flyBehaviour = new DoNotFly();
this.setFlyBehaviour(flyBehaviour);
this.setQuackBehaviour(squeakBehaviour);
}
display() {
// displays rubber duck
}
}
class DecoyDuck extends Duck {
constructor() {
const squeakBehaviour = new Mute();
const flyBehaviour = new DoNotFly();
this.setFlyBehaviour(flyBehaviour);
this.setQuackBehaviour(squeakBehaviour);
}
display() {
// displays rubber duck
}
With this refactoring, we have achieved our goals of code reuse and flexible design by using composition instead of inheritance, and this pattern is called Strategy Pattern
.