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.
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.