Composition over Inheritance, the importance of context

TL;DR:

When the GoF coined

Favor object composition over class inheritance

they were referring to a specific context, and using the definitions of composition and inheritance they set up in this context. If you use this quote to justify a practice, your definitions should match those. If you have another personal definition of object composition, good for you, however, please don’t think your own practice is therefore endorsed by “the book” and don’t trick other people into thinking so. Above all, always describe the pitfalls when teaching something.

Keep reading if you want to know why I insist on this.

The problem

There have been several confused discussions over the past few months, especially in the javascript community on reddit, around the meaning of composition and inheritance in a dynamic language OOP context.

Those discussions mostly stemmed from this article by Eric Elliott and this video.

And what those two are doing is: strongly advocating composition over inheritance while using a technique which is a form of inheritance.

But by a sleight of hand and a bit of rhetorical musing they manage to hide from a beginner’s sight the inheritance issues still lying in their code. Those very issues that compositional patterns from the GoF book are meant to solve.

Ironically, Eric Elliott is even calling the technique concatenative inheritance (and I agree with him on the fact that we should use this term instead of others like mixins, so that the inheritance part is clearly apparent).

Now, to be clear, I don’t have anything against concatenative inheritance combined with factories, it’s a very useful pattern in javascript. If you known what you’re doing when using it.

So, I’m glad that this pattern is taught, but my concern is the way it’s taught. Beginners can take the whole thing and apply it everywhere, confident that this will forever free them of inheritance troubles. And then learn the hard way that inheritance used incorrectly bites you and harms your codebase and therefore, your colleagues.

My main point being for this part: insist on the possible misuses and pitfalls when showing off a technique. You may be well aware of them, that’s not the case of your whole audience.

In the rest of this article, I’ll explain what are those inheritance pitfalls that composition (in GoF sense) tries to avoid, and I’ll show that concatenative inheritance still carries them.

The GoF definition

So, “favor object composition over class inheritance” is a quote from the GoF book. As such, it has a context, and it applies in this context. Of course, the GoF defined this context and explained what issues they were trying to solve:

As we’ve explained, class inheritance lets you define the implementation of one class in terms of another’s. Reuse by subclassing is often referred to as white-box reuse . The term “white-box” refers to visibility: With inheritance, the internals of parent classes are often visible to subclasses.

Object composition is an alternative to class inheritance. Here, new functionality is obtained by assembling or composing objects to get more complex functionality. Object composition requires that the objects being composed have well-defined interfaces. This style of reuse is called black-box reuse , because no internal details of objects are visible. Objects appear only as “black boxes.” Object composition is defined dynamically at run-time through objects acquiring references to other objects. parent classes often define at least part of their subclasses’ physical representation. Because inheritance exposes a subclass to details of its parent’s implementation, it’s often said that “inheritance breaks encapsulation” [Sny86] . The implementation of a subclass becomes so bound up with the implementation of its parent class that any change in the parent’s implementation will force the subclass to change.

What does that mean?

for the GoF, the major difference between composition and inheritance was that with inheritance, the child gets the whole parent’s structure as its own, whereas in composition, the composite object uses its components through references.

for the GoF, the main issue with inheritance was parent/child tight coupling and loss of control of child over its structure and behavior, not only big object hierarchies like Eric Elliott states. It only takes one level of inheritance to get the issue.

Concatenative inheritance doesn’t solve what GoF’s composition solves

Let’s focus on what’s showed in the video. It starts with showing a brittle class hierarchy, then it breaks it down into more small, focused objects with a single responsibility and uses those to create other objects by merging together the smaller ones.

Now don’t get me wrong, defining objects with the smallest possible interface is good advice, and yes, it solves some issues. But it keeps other ones. What I call a sleight of hands here is that the chosen example is way too simple to show real inheritance troubles.

Let’s change requirements a bit. Let’s say I still want a dog object like in the video, which will be a pooper and a eater. However, a pooper can only poop once every given amount of time, and a eater can only eat once every given amount of time. To add more fun, I’m in the team responsible for the dog implementation, but the pooper and eater functionalities are developed by other teams and provided in modules they maintain.

Here is what they provide to you:

const pooper = (delay) => ({

lastPoop: null,

delay: delay,

canPoop: function(now) {

return this.lastPoop ? (this.lastPoop + this.delay) >= now : true

}

poop: function(){

if(this.canPoop(new Date())){

//implementation for pooping is omitted

this.lastPoop = new Date();

}

}

}); const eater = (delay) => ({

lastMeal: null,

delay: delay,

canEat: function(now) {

return this.lastMeal ? (this.lastMeal + this.delay) >= now : true

}

eat: function(food){

if(this.canEat(new Date())){

//implementation for eating is omitted

this.lastMeal = new Date();

}

}

});

See the problem here? The two actions depend on a “delay” field. If you use the concatenative inheritance like suggested, using Object.assign to merge a pooper and a eater to create your dog, then your dog is submitted to the same delay for pooping and eating.

const dog = (poopDelay, eatDelay, pooper, eater) => Object.assign({}, pooper(poopDelay), eater(eatDelay)); let myDog = dog(4, 8, pooper, eater);

console.log(myDog.delay) //8

That’s exactly the kind of issue that composition in the GoF sense is meant to solve and that concatenative inheritance doesn’t solve. Object composition is not object merging. With composition:

const compositeDog = (poopDelay, eatDelay, pooper, eater) =>({

pooperImpl: pooper(poopDelay),

eaterImpl: eater(eatDelay),

poop: () => this.pooperImpl.poop(),

eat: (food) => this.eaterImpl.eat(food)

})

Here the composite object gets the implementation objects as attributes, and then uses them to take only the functionality it needs and expose the interface you want.

That’s the kind of flexibility composite objects really gives you, that merged objects can’t.

It doesn’t mean composition is always the right solution. It adds complexity to your implementation (having to add properties to your object and proxy what you need). If you have full control over the code, merging can be fine. And that’s why GoF used the term “favor”.

(yo can check e.g. this article to get hints about when using one over the other)

Conclusion

Context is important. Don’t use a quote from someone if you don’t give the same meaning to the terms. And if you have any influence over other people, especially beginners, don’t push confusion onto them for the sake of it, that’s throwing them under the bus.

I’m not a native English speaker, don’t hesitate to point out any spelling or grammatical mistake.