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.