DEV Community

Manav Misra
Manav Misra

Posted on • Edited on

Consider Composition

“Favor object composition over class inheritance”

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

Banana

“...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:

  1. 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.
  2. 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;
  },
});
Enter fullscreen mode Exit fullscreen mode

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}.`;
Enter fullscreen mode Exit fullscreen mode

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(),
});
Enter fullscreen mode Exit fullscreen mode

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,
});
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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]
}
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode
👋🏽. 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]
}
Enter fullscreen mode Exit fullscreen mode

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)