Before we start to understand the limitations of inheritance and how you can solve them with composition it will be good to point out that prototypal inheritance is for object-oriented programming paradigms as closures are for functional programming paradigms and that inheritance is when you design your types around what they are while the composition is when you design your types around what they do.
Side Note: I recommend you to read my previous articles: JavaScript Factories, JavaScript Super Powers – Closures, and JavaScript Super Powers – Prototypal Inheritance.
Let’s say that we design an application that has the classes below:
Car
.drive()
.spin()
Airplan
.drive()
.fly()
Teacher
.teach()
.talk()
Student
.learn()
.talk()
Notice that the talk method and the drive method are duplicated. to prevent it we will lift up the methods to a shared class:
Vehicle
.drive()
Car
.spin()
Airplan
.fly()
Person
.talk()
Teacher
.teach()
Student
.learn()
Now if we will need to create a class named TalkingCar that can drive, spin, and talk. we will get into a problematic situation because we can’t fit the class into the current inheritance hierarchy.
We could create a parent class for the Vehicle and Person class like this:
MainParentObject
.talk()
Vehicle
.drive()
Airplan
.fly()
Car
.spin()
TalkingCar
Person
Teacher
.teach()
Student
.learn()
In this situation, the objects will have a lot of functionality that is not needed. one of the phrases about this situation is the “gorilla banana problem”. it means that you request a banana but you get a gorilla holding the banana and the entire jungle with it.
We could instead duplicate the talk method to TalkingCar and the Person, but we have a better way.
With composition, we can overcome this problem much much better.. instead of designing our types around what they are, we will design them around what they do.
The functional programming paradigm means creating functions that operate on well-defined data structures which in our case are objects, rather than belonging to the data structure like objects in the example above.
So We want to separate the data and the behavior to achieve a dynamic creation of different types of objects. we will create factories with states for each type of object (we can use one factory and return different types of objects by conditions), and we will create functions that will receive state and will return an object with specific behavior.
Let’s first examine the functionalities of each object before we will implement the code:
student => learner + talker
teacher => teaches + talker
airplan => driver + flyer
car => driver + spinner
talkingCar = > driver + spinner + talker
Now Let’s create the functions that will handle the different behaviors:
const learner = (state) => ({
learn: () => console.log(`I am learning ${state.subject}` )
});
const talker = (state) => ({
talk: () => console.log(`Hi, my name is ${state.name}` )
});
const teaches = (state) => ({
teach: () => console.log(`I teach ${state.field}` )
});
const driver = (state) => ({
drive: () => console.log(`I drive at speed ${state.speed}` )
});
const spinner = (state) => ({
spin: () => console.log(`I spin ${state.spinTimes * 360} degree` )
});
const flyer = (state) => ({
fly: () => console.log(`I fly at ${state.height} hight` )
});
The functions above are like factory functions but instead of creating their own state internally, they accept their state as a parameter so thay are able to share the same state.
Now Let’s demonstrate how to create factory for one specific type of object, the talkingCar,:
const talkingCar = (name) => {
let state = {
name,
speed: 60,
spinTimes: 3
};
return Object.assign(
{},
driver(state),
spinner(state),
talker(state)
);
}
The factory creates a state object, and returns an object with the use of “Object.assign”. we passed to “Object.assign” an empty object that will be merged with the rest of the objects that are returned from the functions we pass to it.
As you can see the composition is more dynamic, less restricted, more simple, and probably better. you should consider using composition over inheritance.