Composition over Inheritance and JavaScript

Author
Tim Severien
Categories
Publish date

You might have heard of the “composition over inheritance” principle. It encourages developers to choose object composition over inheritance. With JavaScript, the problems this principle aims to solve can be done in various ways by implementing different paradigms.

In this article we’ll have a look at some of these paradigms, their strengths, weaknesses, and how they can be implemented in JavaScript.

Disclaimer: No animals were hurt in the production of this article.

Inheritance

Inheritance is a mechanism to define an ‘is a’ relationship, often used to reuse code of a parent class in a subclass. JavaScript is a prototype-based language but has syntactic sugar to define classes. Consider the following code:

class Animal {
    eat() { /* ... */ }
}

class Dog extends Animal {
    speak() { /* ... */ }
}

class Snake extends Animal {
    shedSkin() { /* ... */ }
}

The classes Dog and Snake inherit from Animal, so an instance of Dog or Snake can both eat(). A dog can’t shed its skin and I don’t think a snake can speak, so speak() and shedSkin() are defined in Dog and Snake respectively.

This can get complex quickly. Imagine we’re building an open-world science fiction game where the player’s pet dog, Gizmo, protects you from wild animals. Gizmo might get hurt fighting these animals, but the player can replace damaged body parts with mechanical parts.

class DogWithLaserEyes extends Dog {
    // ...
}

class DogWithMechanicTail extends Dog {
    // ...
}

class DogWithLaserEyesAndMechanicTail extends Dog {
    // ...
}

We already run into our first problem: we can’t reuse the code of DogWithLaserEyes and DogWithMechanicTail in the DogWithLaserEyesAndMechanicTail class. We can only inherit from a single class with the ES2015 class syntax.

As we add more components the amount of classes increases exponentially, as in this case, every combination is possible.

Inheritance is a neat and simple way to reuse code and extend classes. It aligns very well with how we, as humans, classify objects. A dog with a mechanical tail is still a dog. Unfortunately, it’s not very scalable. It may also lead to problems like the fragile base class problem, where changes in the state of the base class result in malfunctions in a subclass.

Let’s look at some other paradigms.

Multiple Inheritance, Traits, and Mixins

Some programming languages support multiple inheritance, where a subclass can inherit from several base classes. JavaScript doesn’t have syntax for it, but multiple inheritance can be mimicked by (ab)using prototypes:

function Dog() { /* ... */ }
Dog.prototype = { /* ... */ };

function CyborgWithLaserEyes() { /* ... */ }
CyborgWithLaserEyes.prototype = { /* ... */ };

function DogWithLaserEyes() {
    // Call constructors of parent classes
    Dog.call(this);
    CyborgWithLaserEyes.call(this);
}

DogWithLaserEyes.prototype = { /* ... */ };

// Assign prototype properties of parent classes to DogWithLaserEyes
Object.setPrototypeOf(DogWithLaserEyes.prototype, Object.assign(
    {},
    Dog.prototype,
    CyborgWithLaserEyes.prototype,
));

I think we can agree that the above code isn’t very readable. We also have to deal with hierarchical challenges. When several parent classes have a method with the same name, we have to choose which method we want in our subclass.

Traits and mixins are alternatives to multiple inheritance that can be used to compose a class from several bits and pieces. These two concepts are quite similar, but with small semantic differences. In JavaScript, they can be mimicked. Consider the following example of mixins:

const DogMixin = (Base) => class extends Base {
    speak() { /* ... */ }
    walk() { /* ... */ }
};

const CyborgWithLaserEyesMixin = (Base) => class extends Base {
    speak() { /* ... */ }
};

// A base class is required for the mixins. It may be empty.
class Base { /* ... */ }

class DogWithLaserEyes extends CyborgWithLaserEyesMixin(DogMixin(Base)) {
    // ...
}

The mixins in above code essentially are factory functions that create a new class based on given base class. Because JavaScript is a bit peculiar, we can create classes at runtime too. In this situation, we could dynamically generate them based on what components were applied to our pet dog, Gizmo.

In the above example, we can’t realistically use constructors on the mixin classes. The mixins have no knowledge of the class they extend or final concrete class. That means they don’t know what arguments they could receive or should pass on. Additionally, the order of the mixin calls matter when several mixins have a method with the same name.

Multiple inheritance, traits and mixins all improve reusability but come with drawbacks that can complicate scalability of your codebase. It’s probably because inheritance aligns so well with our way of thinking that many choose it as their go-to way to reuse code.

[O]ur experience is that designers overuse inheritance as a reuse technique

Design Patterns: Elements of Reusable Object-Oriented Software

So far we’ve been doing class composition; composing classes from small bits and pieces. In the phrase “composition over inheritance,” composition refers to object composition. It’s easy to misinterpret the phrase. I think this also explains why many posts about composition over inheritance propose mixins as the solution. It’s actually much simpler.

Object Composition

In contrary to inheritance, object composition defines a ‘has a’ relationship. Clearly, we can replace parts of Gizmo and still consider it a dog. When these properties are separate objects, we can create a simple Dog class and assign arbitrary objects that handle some of the Dogs functionality. Consider the following code:

class CanineSpeakBehavior {
    speak() {
        console.log('Woof');
    }

    warn() {
        console.log('Grrrrr');
    }
}

class Dog {
    constructor(speakBehavior = new CanineSpeakBehavior()) {
        this.speakBehavior = speakBehavior;
    }

    speak() {
        this.speakBehavior.speak();
    }

    warn() {
        this.speakBehavior.warn();
    }
}

In the above code CanineSpeakBehavior is an object that defines the speaking behaviour for the Canine species (Dogs, Wolves, etc). It is the default speaking behaviour for Dogs. The Dog delegates speaking and warning to the CanineSpeakBehavior.

Now we can create an instance of Dog that will bark by default. In the event this dog is enhanced with a speech synthesiser, I can overwrite the speaking behaviour at runtime:

class NoSpeakBehavior { 
    speak() {
        // Nothing
    }
}

class SyntheticSpeechSpeakBehavior { 
    speak() {
        console.log('Life would be tragic if it weren\'t funny');
    }
}

const gizmo = new Dog();

gizmo.speakBehavior = new NoSpeakBehavior();
gizmo.speak(); // Prints nothing

gizmo.speakBehavior = new SyntheticSpeechSpeakBehavior();
gizmo.speak(); // Prints "Life would be tragic if it weren't funny"

Unlike the paradigms we discussed before, with object composition we don’t have to create a new instance of dog but can change things about it. This aligns even better with how we think in real life. Gizmo isn’t a new dog, we merely replace parts.

We can extend the above example by adding more properties to Dog and creating new classes. For example, we could create the classes DogTail and MechanicTail. The DogTail can be the default tail of Dog. We’re encapsulating existing concepts in Dog and we can add new ones indefinitely.

The only downside is that we have a little bit more code to delegate an action to another object, but we get much more control in return.

Because JavaScript is a dynamically typed language, there’s room for error. I can assign any value, like a number to gizmo.speakBehavior and break things. It requires some discipline to not make mistakes. You could duck test behaviour when assigning the value:

class Dog {
    // ... 

    set speakBehavior(value) {
        if (typeof value.speak !== 'function') {
            throw new Error(`"${value}" does not have a speak() method`);
        }

        if (typeof value.warn !== 'function') {
            throw new Error(`"${value}" does not have a warn() method`);
        }

        this._speakBehavior = value;
    }
}

Alternatively, we could leverage TypeScript to get the benefits of static types. Consider the following code:

interface ISpeakBehavior {
    speak(): void;
    warn(): void;
}

class NoSpeakBehavior implements ISpeakBehavior {
    // ...
}

class SyntheticSpeechSpeakBehavior implements ISpeakBehavior {
    // ...
}

class Dog {
    public speakBehavior: ISpeakBehavior;

    // ...
}

const gizmo = new Dog();

// This is fine
gizmo.speakBehavior = new SyntheticSpeechSpeakBehavior();

// TypeScript will throw an error on compilation
gizmo.speakBehavior = 707;

Composition Over Inheritance in the Real World

Admittedly, not many of us are game developers and we probably don’t write code for cyborg dogs all that often. Imagine we’re working on a front-end and we have several forms.

Some need to be validated via an asynchronous HTTP request. Let’s write a component to do that:

class ValidateFormComponent {
    constructor(form) {
        const onFormChange = event => this.updateFieldValidation(event.target);

        this.element = form;
        this.element.addEventListener('change', onFormChange);
        this.element.addEventListener('input', onFormChange);
    }

    updateFieldValidation(field) {
        // Do HTTP request, show error messages, etc
        // ...
    }
}

Other forms have an impact on important data. To make sure the user doesn't submit the form accidentally, we wish to show a confirm box before the form is submitted. Let’s create another component and call it ConfirmFormComponent:

class ConfirmFormComponent {
    constructor(form) {
        this.element = form;
        this.element.addEventListener('submit', event => this.confirm(event));
    }

    confirm(event) {
        event.preventDefault();

        if (window.confirm('Are you at least 90% sure?')) {
            this.element.submit();
        }
    }
}

A while later we’re working on a product deletion form that requires some information we must validate. We already have components to validate fields and confirm the submission of a form. We’re pretty smart and realise we can reuse those components for our product deletion form.

Inheritance can’t be used as we can’t extend two classes. We could combine the two components in a single one and toggle functionality based on the presence of some attributes, resulting in one big component, and extend it for our new component. We could also convert the components to mixins, but we would introduce the concept of mixins to the codebase and maybe have to train some team members.

None of the options above seems like a good idea. Object composition, on the other hand, doesn’t require us to change our existing code and only encapsulates the components:

class ProductDeletionFormComponent {
    constructor(form) {
        this.element = form;

        this.confirmFormComponent = new ConfirmFormComponent(this.element);
        this.validateFormComponent = new ValidateFormComponent(this.element);

        // ...
    }
}

Although there is no ‘has a’ relationship here, as we’re not nesting forms, our ProductionDeletionFormComponent has the same functionality we can easily delegate. Note that these components manage their own state and we don’t need to delegate any methods. Should that change, we can do that with very little effort.

Conclusion

Even though code is relatively simple when conceived, it quickly gets complex after a couple of additions. As we have observed in this article, inheritance rarely is a scalable solution when dealing with structurally complex code. This is true for any language–JavaScript included–despite its flexibility and ability to mimic other paradigms.

Using composition over inheritance and having object composition as your go-to mechanism to reuse code is a great practice. It prevents a lot of hierarchical puzzling and often results in better decoupling.

Back to top

Accept cookies?

We are actively scouting for new talent to join us and would like to remind you outside of Vi. With your consent, we place small files (also known as 'cookies') on your computer that will allow us to do that.

Find out more about cookies or .

Manage cookie preferences