DEV Community

Tom Hoadley
Tom Hoadley

Posted on • Updated on

Software Patterns: Composition vs Inheritance

“Favor object composition over class inheritance”

The Gang of Four, “Design Patterns: Elements of Reusable Object Oriented Software”

Before getting started, it's worth noting that it will help to have a basic understanding of JavaScript and of ES6 new features to follow along with this tutorial. If you don't you should still hopefully be able to pick something up and can always brush up on ES6 afterwards!

When building your software you have a choice of how to structure your code. These choices make a big difference over the life of your product, from the development of it to the future maintenance. There are an array of different well known patterns and it is rarely a case of selecting just one and sticking to it. However, for the purpose of learning,  this blog post will present two different patterns, composition and inheritance and why the composition pattern is almost always preferable over the inheritance pattern.

This post should help give you a mental model to remember composition with the following examples with the use of super humans, because why not?! The first example displays how you would use the inheritance pattern to create super humans This will identify the limitations of the inheritance pattern, which will then be addressed by the composition pattern used in second example.

The Inheritance Pattern

Firstly we create a base class called superHuman, which has a default ability to fly.

    class superHuman {
        constructor(name) {
            this.name = name;
        }
        fly() {
            console.log(`${this.name} is flying`);
        }
    }

    const superGuy = new superHuman('Super Guy');

    superGuy.fly();

    // Super Guy is flying;

We then realise that being able to fly isn’t enough so we want to give the super humans a few other abilities, such as laser eyes and turning invisible. With the inheritance pattern, we would write the following.

    class laserSuperHuman extends superHuman {
        lasers() {
            console.log(`${this.name} has fired lasers from eyes`);
        }
    }

    class invisibleSuperHuman extends superHuman {
        turnInvisible() {
            console.log(`${this.name} is now invisible`);
        }
    }

With this new code, we can create an improved version of Super Guy who can shoot lasers as well as fly.

    const superGuy2 = new laserSuperHuman('Super Guy v2');
    superGuy2.fly();
    superGuy2.lasers();
    // Super Guy v2 is now flying
    // Super Guy v2 has fired lasers from his eyes

That’s all fine but when we try to give him the ability to turn invisible as well as shoot lasers and fly we run into a problem because we can’t do the following.

    // class laserInvisibleSuperHuman extends laserSuperHuman , invisibleSuperHuman {
    // ... 
    // }

This is where the composition pattern comes into play.

The Composition Pattern

Instead of using the above pattern we could refactor our code to the following, which will allow us to give them the different abilities we want with ease.

Firstly, we write a function which returns our super human as an object.

    function createBaseSuperHuman(name) {
        const superHuman = {
            name: name,
            fly: () => console.log(`${name} is flying`)
        }
        return superHuman
    }

    const superGuy = createBaseSuperHuman("Super Guy");
    superGuy.fly();

    // Super Guy is flying

Now we want to add our abilities, but this time they aren’t an inheritance of a base class and instead are stand alone functions.

 function shootLaser({ name }) {
        return {
            shootLaser: () => console.log(`${name} shot lasers from eyes`)
        }
    }

    function turnInvisible({ name }) {
        return {
            turnInvisible: () => console.log(`${name} turned invisible`)
        }
    }

    function grow({ name }) {
        return {
            grow: () => console.log(`${name} has grown to 100ft tall`)
        }
    }

Doing it this way, we can now pick and choose the abilities we want to apply to our super human. For example, maybe we want Super Guy to not only fly and shoot lasers, but also turn invisible and grow. (Marvel, get in touch for licensing.)

   function growingLaserShootingInvisibleFlyingSuperHuman(name) {
        const superHuman = createBaseSuperHuman(name);

        return {
            ...superHuman,
            ...shootLaser(superHuman),
            ...turnInvisible(superHuman),
            ...grow(superHuman),
        }
    }

As you can see using the spread operator (…), we have a way of applying whatever abilities we want to it.

   const superGuyV2 = growingLaserShootingInvisibleFlyingSuperHuman('Super Guy v2');

    superGuyV2.fly();
    superGuyV2.shootLaser();
    superGuyV2.turnInvisible();
    superGuyV2.grow();

    // Super Guy v2 is flying
    // Super Guy shot lasers from his eyes
    // Super Guy v2 turned invisible
    // Super Guy v2 has grown to 100ft tall

Conclusion

As you can see from above, the composition pattern provides a much more robust, maintainable method of writing software and is a principle that you will see throughout your career in software engineering.

Top comments (0)