Composition vs Inheritance

Hey ,

I'm thrilled to help you learn JavaScript. Unfortunately, you've landed on a page where you cannot access with your current purchase.

Please upgrade (use this link) access this content.

I'm super eager to help you learn more!

Composition vs Inheritance

Imagine you want to create a Developer and Designer. They have the following characteristics:

  1. Both Developer and Designer should have firstName and lastName
  2. Both Developer and Designer should have the ability to sayName
  3. Developers can code, but Designers can’t.
  4. Designers can design, but Developers can’t.

How would you create this?

Here’s one possibility: We can create a Human class that contains firstName, lastName, and sayName.

class Human {
  constructor (firstName, lastName) {
    this.firstName = firstName
    this.lastName = lastName
  }

  sayName () {
    console.log(`${this.firstName} ${this.lastName}`)
  }
}

Since Developers are Humans, we can extend the Human class to create a Developer class.

class Developer extends Human {
  // ...
}

We’ll add code to Developer since developers can code.

class Developer extends Human {
  code (thing) {
    console.log(`${this.firstName} coded ${thing}`)
  }
}

Designers are also human. We can extend Human to create a Designer class.

class Designer extends Human {
  // ...
}

We’ll add the design method to Designer since designers can design.

class Designer extends Human {
  design (thing) {
    console.log(`${this.firstName} designed ${thing}`)
  }
}

Inheriting from two Classes

Let’s say there’s a person who can design and code. How would you create this person?

  1. You can’t use Developer since developers cannot design.
  2. You can’t use Designer since designers cannot code.

Maybe you can create a subclass from both classes?

We’ll use DesignerWhoCodes as a constructor for this person (but it can also be CoderWhoDesigns or Unicorn or something else you prefer).

// Doesn't work
class DesignerWhoCodes extends Designer, Developer {
  // ...
}

Unfortunately, this doesn’t work in JavaScript.

JavaScript doesn’t let you inherit from multiple classes because it uses Prototypal Delegation.

You can only delegate to one prototype.

Composing Using Mixins

One way to create DesignerWhoCodes is to use Mixins. Mixins are objects that contain properties you want to “mix” into another object.

To use Mixins, we have to redesign Human. We will place all skills (like sayName, design, and code) in separate objects.

// Skills
const canCode = {
  code (thing) {
    console.log(`${this.firstName} coded ${thing}`)
  }
}

const canDesign = {
  design (thing) {
    console.log(`${this.firstName} designed ${thing}`)
  }
}

const canSayName = {
  sayName () {
    console.log(`${this.firstName} ${this.lastName}`)
  }
}

Next, we will construct a Human with firstName and lastName properties.

class Human {
  constructor (firstName, lastName) {
    this.firstName = firstName
    this.lastName = lastName
  }
}

We can create a Developer by extending Human like before.

class Developer extends Human {}

Developers should be able to code and sayName. We can mix canCode and canSayName into Developer to give these skills to Developer.

We mix the skills into Developer’s prototype with Object.assign.

Object.assign(Developer.prototype, canCode, canSayName)

A Developer instance can now sayName and code. But it cannot design.

const zell = new Developer('Zell', 'Liew')

zell.sayName() // Zell Liew
zell.code('website') // Zell coded website
zell.design('website') // Error
Developer instance.

We can repeat the same process to create a designer. We will extend Human to create Designer.

class Designer extends Human {}

Designers can sayName and design, but they cannot code. We can mix these skills in with Object.assign.

Object.assign(Designer, canDesign, canSayName)

A Designer instance can now design and sayName. But it cannot code.

const vincy = new Designer('Vincy', 'Zhang')

vincy.sayName() // Vincy Zhang
vincy.design('website') // Vincy designed website
vincy.code('website') // Error
Designer instance.

We can (once again) repeat the same steps to create DesignerWhoCodes.

class DesignerWhoCodes extends Human {}
Object.assign(DesignerWhoCodes, canCode, canDesign, canSayName)

A DesignerWhoCodes instance can sayName, code, and design.

const tracy = new DesignerWhoCodes('Tracy', 'Lim')

tracy.sayName() // Tracy Lim
tracy.code('website') // Tracy coded website
tracy.design('website') // Tracy designed website
DesignerWhoCodes instance.

Mixins uses Copy-pasting

When we combine objects with Object.assign, we copy properties from the later object into the earlier object. Therefore, Mixins uses Copy-pasting or Concatenative Inheritance.

Mixins and Composition

Composition means to combine things together. In this case, we combine skills (each skill is an object) into the subclasses prototype (another object).

Since we combine objects, we can say Mixins uses Object Composition.

// Object Composition
Object.assign(object1, object2)

There is another form of composition in JavaScript known as Function Composition, where we combine functions together to make a new function.

// Function Composition
const finalFunction = compose(function1, function2)

Don’t mix up Object Composition and Function Composition! (We won’t cover Function Composition much since we’re not touching Functional Programming in this course).

Favour Composition over Inheritance

When people say favour composition over inheritance, they mean the following terms:

  1. Composition means Object Composition
  2. Inheritance means Subclassing

They actually mean favour Object Composition over Subclassing.

Why?

When you design software, it’s impossible to know all future requirements in advance. Things will change. You’ll have to make adjustments. When you compose objects, you can create derivative objects without limiting yourself to a fixed structure.

Example:

Let’s say we have a new requirement now. We need a Robot who can design and code. But Robot cannot sayName.

How would you create Robot?

It’s super hard to create Robot if you used subclasses. But if you composed skills into the derivative object, you can always create a new Robot. For example, we can extend Human to create Robot, then give it canCode and canDesign.

class Robot extends Human {}
Object.assign(Robot.prototype, canCode, canDesign)

This works… but it’s weird for Robot to be a subclass of Human… right? With this, we see another downside with subclassing: It can be really hard to name abstractions.

Skipping Subclassing entirely

We can skip the entire extends Human thing and create Robot as a new Class.

When we do this, we can become even more flexible. For example, let’s say our Robot only has a firstName. It doesn’t have lastName.

class Robot {
  constructor (firstName) {
    this.firstName = firstName
  }
}

Object.assign(Robot.prototype, canCode, canDesign)

A Robot instance can still code and design. And it still doesn’t know how to sayName.

const KD82 = new Robot('KD82')
KD82.sayName() // Error
KD82.code('website') // KD82 coded website
KD82.design('website') // KD82 designed website

Composition via OLOO

You can compose mixins into OLOO with Object.assign as well.

const Human = {
  init (firstName, lastName) {
    this.firstName = firstName
    this.lastName = lastName
    return this
  }
}

const Developer = Object.create(Human)
Object.assign(Developer, canCode, canSayName)

Developer instances will now be able to code and say their name. But they won’t be able to design.

const zell = Object.create(Developer).init('Zell', 'Liew')

zell.sayName() // Zell Liew
zell.code('website') // Zell coded website
zell.design('website') // Error
Developer instance.

Composition via Factory Functions

You can also use Object.assign to compose mixins into Factory Functions.

function Human (firstName, lastName) {
  return {
    firstName,
    lastName
  }
}

function Developer (firstName, lastName) {
  const developer = Human(firstName, lastName)
  return Object.assign(developer, canCode, canSayName)
}

Developer instances will now be able to code and say their name. But they won’t be able to design.

const zell = Developer('Zell', 'Liew')

zell.sayName() // Zell Liew
zell.code('website') // Zell coded website
zell.design('website') // Error
Developer instance.

Is Subclassing and Prototypal Delegation useless then?

Nope! Prototypal Delegation is a design pattern. It’s up to you whether you want to use Composition (and hence use Copy-paste) or use Prototypal Delegation.

When to favour Inheritance over Composition

You want to favour Inheritance over Composition when you know the derivative object completely inherits all properties of the parent object.

There are lots of great examples of this in the wild.

For example, Array derives from Object. If you look at an Object’s prototype, you see methods like hasOwnProperty, isPrototypeOf, toString, valueOf, and others.

Object Prototype

If you open up an Array’s prototype, you’ll see methods you’re familiar with, like find and slice.

Array prototype

If you open an Array prototype’s prototype, you’ll see the same properties Object’s prototype.

Second level into Array.prototype

This is why we say Array is an Object. (In fact, everything are objects in JavaScript, including primitives like strings. Try writing new String and explore!).

Another example: Go look up HTMLElement on MDN. You’ll see this flowchart.

HTMLElement on MDN.

What this says is:

  1. HTMLElement inherits from Element
  2. Element inherits from Node
  3. Node inherits from EventTarget

This means:

  1. Nodes will have properties available to EventTargets
  2. Element will have properties available to EventTarget and Node
  3. HTMLElement will have properties available to EventTarget and Node and Element.

And if you dig further, you’ll notice EventTarget contains three methods:

  1. EventTarget.addEventListener()
  2. EventTarget.removeEventListener()
  3. EventTarget.dispatchEvent()

Now you understand why HTML Elements have addEventListener and removeEventListener? 😄