“Favor object composition over class inheritance”
the Gang of Four, “Design Patterns: Elements of Reusable Object-Oriented Software”
“...the problem with object-oriented languages is they’ve got all this implicit environment that they carry around with them. You wanted a banana but what you got was a gorilla holding the banana and the entire jungle.”
∼ Joe Armstrong, “Coders at Work”
Overview
Rather than focusing on the OOP capabilities of JS, many folks come to appreciate the relative simplicity and ease of using composition over inheritance.
Put simply:
- Inheritance focuses on establishing 'is a' relationships. Although this practice somewhat dominates the industry, one of the drawbacks is that we must carefully try to plan and anticipate many use cases for 'things' ahead of time. It's not too difficult to overlook something and then have to struggle to backtrack.
- Composition is more about 'has a' relationship - or we focus on what something should be able to do. Essentially, we 'mix in' or compose whatever functionality from wherever we might need it, 'on the fly.' This tends to make us a bit more adaptable, and even the syntax and implementation can be easier to follow.
This video is a great 'primer' for the concepts expressed here.
Also, as a reference to the 'inheritance version' of the code I go through here:
Composition
In a previous post, we explored creating new objects 'on the fly_ using object composition. Now we'll take it to the next level.
Function Factories 🏭 to Encapsulate 'Functionality'
Again, in order to build up a Student
or Faculty
member, we will want to compose them from some other objects that represent different pieces of functionality.
const Greeter = (name) => ({
greet() {
return `👋🏽. My name is, ${name}.`;
},
});
const CourseAdder = () => ({
addCourse(newCourse) {
this.currentCourses = [newCourse, ...this.currentCourses];
},
});
const RaiseEarner = () => ({
giveRaise(raiseAmt) {
this.salary += raiseAmt;
},
});
Done! We have 3 'things' that we can compose together to build 'bigger' things. Again, we capitalize to denote that these are 'things' made to be reused or composed (same convention with React components).
Each of these is a function factory that returns an object. that 'wraps' 🎁 a method to implement some specific functionalities.
const Greeter = (name) => ({
greet() {
return `👋🏽. My name is, ${name}.`;
Greeter
encapsulates name
in a closure. It means that after Greeter
is invoked, instead of name
being garbage collected, name
will stick around as long as it's needed by greet
.
We'll see 👇🏽 that Greeter
will be composed into Student
and Faculty
. Whenever we create a Student
, for example, the name
that we use will be 'enclosed' in greet
. Each time greet
is run 🏃🏽♂️ on a Student
, that 'enclosed' name
will be referenced.
Student
and Faculty
const Student = ({ id, name, age, major, credits, gpa, currentCourses }) => ({
id,
name,
age,
major,
credits,
gpa,
currentCourses,
...Greeter(name),
...CourseAdder(),
});
const Faculty = ({ id, name, age, tenured, salary }) => ({
id,
name,
age,
tenured,
salary,
...Greeter(name),
...RaiseEarner(),
});
Both Faculty
and Greeter
'mix in' 👩🏽🍳 the desired functionalities. For example: ...Greeter(name),
. We can say that each of these has a Greeter
.
When we mix in Greeter
, we are binding the name
- there's that closure.
RaiseEarner
and CourseAdder
are invoked and bound with a this
- this.currentCourses
and this.salary
.
Next, we'll instantiate mark
, an instance of Student
and richard
, an instance of Faculty
.
const mark = Student({
id: 1124289,
name: "Mark Galloway",
age: 53,
major: "Undeclared",
credits: {
current: 12,
cumulative: 20,
},
gpa: {
current: 3.4,
cumulative: 3.66,
},
currentCourses: ["Calc I", "Chemistry", "American History"],
});
const richard = Faculty({
id: 224567,
name: "Richard Fleir",
age: 72,
tenured: true,
salary: 77552,
});
All we do is pass in their unique properties, our function factories crank them out.
Testing things out:
mark.addCourse("Psych");
richard.giveRaise(5000);
console.log(mark.greet(), richard.greet());
console.log(mark, richard);
We can see:
👋🏽. My name is, Mark Galloway. 👋🏽. My name is, Richard Fleir.
{
id: 1124289,
name: 'Mark Galloway',
age: 53,
major: 'Undeclared',
credits: { current: 12, cumulative: 20 },
gpa: { current: 3.4, cumulative: 3.66 },
currentCourses: [ 'Psych', 'Calc I', 'Chemistry', 'American History' ],
greet: [Function: greet],
addCourse: [Function: addCourse]
} {
id: 224567,
name: 'Richard Fleir',
age: 72,
tenured: true,
salary: 82552,
greet: [Function: greet],
giveRaise: [Function: giveRaise]
}
The Main Advantage Over Inheritance
I prefer this implementation better, both syntactically and conceptually. I like to think of what things 'do' rather than what they 'are.'
But...here's a 'real' advantage. What if, we have a Student
...that gets hired on to teach also? So...a FacultyStudent
- one that can add courses and/or might get a raise.
A possibly unpredicted situation such as this really shows where composition shines ✨.
More Inheritance? 👎🏽
Maybe. But...what is a FacultyStudent
...should it extend from Student
or Faculty
? In classical OOP, this is where you might create an interface - a means to encapsulate functionality that can be implemented by various classes. Fine.
More Composition 👍🏽
How about this instead?
const FacultyStudent = ({
id,
name,
age,
major,
credits,
gpa,
currentCourses,
tenured,
salary,
}) => ({
id,
name,
age,
major,
credits,
gpa,
currentCourses,
tenured,
salary,
// Composition!
...Greeter(name),
...CourseAdder(),
...RaiseEarner(),
});
const lawrence = FacultyStudent({
id: 1124399,
name: "Lawrence Pearbaum",
age: 55,
major: "CIS",
credits: {
current: 12,
cumulative: 0,
},
gpa: {
current: 0.0,
cumulative: 0.0,
},
currentCourses: ["JavaScript I"],
tenured: false,
salary: 48000,
});
lawrence.addCourse("JavaScript II");
lawrence.giveRaise(2000);
console.log(lawrence.greet(), lawrence);
👋🏽. My name is, Lawrence Pearbaum. {
id: 1124399,
name: 'Lawrence Pearbaum',
age: 55,
major: 'CIS',
credits: { current: 12, cumulative: 0 },
gpa: { current: 0, cumulative: 0 },
currentCourses: [ 'JavaScript II', 'JavaScript I' ],
tenured: false,
salary: 50000,
greet: [Function: greet],
addCourse: [Function: addCourse],
giveRaise: [Function: giveRaise]
}
Essentially, we can just keep applying the same concepts over and over and compose 'on the fly'... "all night long!" 🎵
All together now, and IK that was a lot! Thanks for sticking around.
Updated 1: If this topic is of interest to you, I refer you to this excellent article that's a few years old but still presents some of the same information in a slightly different way:
Top comments (0)