DEV Community

Cover image for Composition > Inheritance in 4 mins
Gavin Macken
Gavin Macken

Posted on

Composition > Inheritance in 4 mins

JavaScript polymorphic behavior with ES6

Composition over inheritance is the principle that classes should achieve polymorphic behavior and code reuse by their composition rather than inheritance from a base.

Inheritance

To better understand why we might favor composition over inheritance let’s first look at inheritance in Javascript, specifically ES6. The extends keyword is used in class declarations or class expressions to create a class that is a child of another class.

class Plant{
 constructor(name){
  this.name = name
 }

 water(){
    console.log("Water the " + this.name)
 }

 repot(){
    console.log( "Repot the " + this.name)
 }
harvest(){
    console.log("Harvest the " + this.name)
  }
}

class Vegetable extends Plant {
  constructor(name, size, health){
   super(name)
   this.health = health;
  }
}

class Flower extends Plant {
  constructor(name, size, health){
   super(name)   
   this.health = health;
  }
}

class Fruit extends Plant {
  constructor(name, size, health){
   super(name)
   this.health = health;
  }
}
Enter fullscreen mode Exit fullscreen mode

We see a potential problem beginning to form using the inheritance pattern.

The water method is shared among the instances of Flower, Vegetable and Fruit which is helpful since they will all need to be watered, but there is no need for an instance of Flower to have access to the harvest method and my vegetables are planted in the ground so there is no reason for them to have access to the repot method.

The associations should look like this:

  • Fruits are watered, repotted, harvested
  • Flowers are watered repotted
  • Vegetables are watered, harvested

OK, so what if I do something like this

class Plant{
  constructor(name){
   this.name = name
  }

 water(){
    console.log("Water the " + this.name)
 } 
}

class Vegetable extends Plant {
  constructor(name, size, health){
   super(name)
   this.health = health;
  }
  harvest(){
    console.log("Harvest the " + this.name)
  }
}

class Flower extends Plant {
  constructor(name, size, health){
   super(name)   
   this.health = health;
  }
  repot(){
    console.log( "Repot the " + this.name)
  }
}

class Fruit extends Plant {
  constructor(name, size, health){
   super(name)
   this.health = health;
  }

  repot(){
    console.log( "Repot the " + this.name)
  }
  harvest(){
    console.log("Harvest the " + this.name)
  }
}
Enter fullscreen mode Exit fullscreen mode

This is a little better, but now we end up creating duplicate methods on the different instances that are doing the same thing, not adhering to DRY principles. This is a problem that can be created by the inheritance pattern.

The problem with object-oriented languages is they’ve got all this implicit environment that they carry around with them. You wanted a banana but what you got was a gorilla holding the banana and the entire jungle. — Joe Armstrong. Creator of Erlang.

Inheritance is by its nature tightly coupled compared to composition. An inheritance pattern forces us to predict the future and build a taxonomy of types. So unless we can predict the future we are invariably going to get a few things wrong.

Composition

A compositional pattern can help us here.

const harvest = () => {
  console.log("Harvesting")
}
const water = () => {
  console.log("Watering")
}
const repot = () => {
  console.log( "Repotting")
}
const Flower = (name) => {
 return Object.assign(
  {name},
  water(),
  repot()
  )
}
const Vegatable = (name) => {
 return Object.assign(
  {name},
  water(),
  harvest()
  )
}
const Fruit = (name) => {
 return Object.assign(
  {name},
  water(),
  repot(),
  harvest()
  )
}
const daffodil = Plant();
daffodil.harvest() // undefined
const banana = Fruit();
banana.harvest() // Harvesting
Enter fullscreen mode Exit fullscreen mode

By favoring composition over inheritance and thinking in terms of what things do rather than what things are, you can see that we have freed ourselves from the tightly coupled inheritance structure.

We no longer need to predict the future because additional methods can be easily added and incorporated into separate classes.

One thing you may notice is that we no longer rely on prototypical inheritance and instead we use functional instantiation to create the object. Once instantiated a variable loses its connection to the shared methods. So, any modification there will not be passed along to instances instantiated before the change.

If this is a problem we can still use prototypal inheritance and composition together to add new properties to prototypes after they are created and thus making them available to all the objects which delegate to that prototype.

An arrow function expression can no longer be used since it does not have a constructor method build in.

function Vegetable(name) {
  this.name = name
 return Object.assign(
    this,
    water(),
    harvest()
  )
}
const Carrot = new Vegetable('Carrot')
Enter fullscreen mode Exit fullscreen mode

To Conclude

Composition is helpful when we are describing a “has a” relationship, while inheritance is useful in describing a “is a” relationship.

Both encourage code re-usability. On occasion, depending on the requirements and solution an inheritance can make sense.
But the vast majority of solutions will require you not just to think about the current requirements but what requirements will be needed in the future, in which case composition should more often than not win out the day.

And there we have it. I hope you have found this useful and thank you for reading. If you enjoyed this and found this helpful you may also enjoy some of the swag ideas we created at !!nerdy. New designs are launched every month.

Top comments (0)