Let’s look at using composition instead of classical inheritance in JavaScript.

JavaScript is an expressive language and is one reason I enjoy using it. An interesting feature is the ability to compose objects from simple objects without inheritance.

Lamborghini Huracan with text "composition over classical inheritance"

What is the difference between inheritance and composition?

Inheritance typically creates a is-a relationship and composition creates a has-a relationship. Composition allows us to naturally build complex objects from simple components making it easier to reason about, rather than trying to identify commonality between classes and building a complex relational structure.

Inheritance is when a class is based on another using the same implementation. A Lamborghini (subclass) would gain methods and properties from a vehicle (superclass) like brake and accelerate. The Lambo will include its own properties like colour. This creates a relationship of a Lamborghini is a vehicle.

Composition is about taking simple objects and combining them to build more complex ones. To build a Lamborghini you might define a function for constructing essential features like engine, design and brakes. This creates a relationship of a Lamborghini has a engine, brakes and design.

Mixins is a way of achieving inheritance

An example of inheritance is mixins because lamboShell object derives its methods from the vehicleMixin. This is essentially copying properties and methods from one object to another. This is one way to achieve inheritance and create a relationship of lambo is a vehicleMixin

Below is an example of creating a mixin:

const vehicleMixin = {
  set (name, value) {
    this[name] = value;
  },

  get (name) {
    return this[name];
  },
  
  speed: 0,
  
  accelerate () {
    this.speed += 2;
    console.log(`accelerated: ${this.speed}`);
  },
  
  brake () {
    this.speed -= 4;
    console.log(`braking: ${this.speed}`);
  }
};

const lamboShell = { colour: 'orange', speed: 0 };
// combines both objects into one
const lambo = Object.assign({}, lamboShell, vehicleMixin);
// the lambo can now accelerate
lambo.accelerate();

// colour is not truly private can be accessed directly via lambo.colour
console.log(lambo.get('colour'));

lambo.colour = 'silver'; // will change value potentially breaking the state

Using ES6 Object.assign() this copies one or more objects to a target object and returns the target object. Like in the example above it is best to start with an empty object {} as this will become the context (this) so that no origin properties become mutated. If lamboShell was the first parameter in Object.assigns then those properties would mutate as well as on the new object. Lodash _.extend() achieves the same result if you need older browser support.

Composition, piecing it together

Taking a different approach to promoting composition, the code below defines a function Lambo that we can pass in expected car features like an engine. This is a basic implementation of Dependency Injection and uses private fields to reference the newly injected objects. Lambo implements its own features using the Engine for example to slowDown or speedUp, adjusting the speed of the Lambo as defined below.

We can then utilise privilege methods to manipulate the private data fields.

Refactoring the example above to compose a Lambo:

const Engine = {
  accelerate (speed, incrementSpeed) {
    return speed + incrementSpeed;
  },
  decelerate (speed, decrementSpeed) {
    return speed - decrementSpeed;
  }
}

const Breaks = {
  stop(speed) {
    if(speed > 0) this.stop(speed - 3);
    
    return 0;
  }
}

const Design = {
  colour: 'Orange',
  model: 'Huracan Spyder'
};

const Lambo = function(Design, Engine, Breaks){
  const design = Object.create(Design);
  const engine = Object.create(Engine);
  const breaks = Object.create(Breaks);
  const props = {
    speed: 0,
    colour: design.colour,
    model: design.model
  };
  
  return {
    set (name, value) {
      props[name] = value;
    },

    get (name) {
      return props[name];
    },
    
    log (name) {
      console.log(`${name}: ${props[name]}`)
    },
    
    slowDown() {
      props.speed = engine.decelerate(props.speed, 3);
    },
    
    speedUp() {
      props.speed = engine.accelerate(props.speed, 3);
    },
    
    stop(){
      props.speed = breaks.stop(props.speed);
    }
  }
};

const lambo = Lambo(Design, Engine, Breaks);
lambo.speedUp();
lambo.log('speed'); //-> 3
lambo.slowDown();
lambo.log('speed'); //-> 0

lambo.log('colour'); //-> orange
// we can change the colour
lambo.set('colour', 'black');
// see it has changed
lambo.log('colour'); //-> black

Conclusion

I believe constructing code to be composable makes it easier to reason about - which should improve its readability. There is some overhead of reimplementing methods, for example Lambo.stop is essentially an alias of Brakes.stop, which would not need to be done when using inheritance. However, I think this tradeoff is worth it to make the code easier to follow, especially when an application becomes complex.