JavaScript Classes Are Just Rube Goldberg Machines for Objects

Cory
Geek Culture
Published in
10 min readApr 20, 2021
Rube Goldberg machines are overly complicated devices for simple tasks (Image source)

The whole job of a class is to ultimately create an object. But JavaScript has one of the simplest, most clear and succinct ways of creating an object, and a familiar dead simple way to do it over and over again in a particular way:

{} // an object() => ({}) // a function that returns the same object shape every time.

So how far can we reasonably take this? Well, to paraphrase a sports references, it could go all the way!

// Why do all of this
class SubClass extends BaseClass {
#state = {}

constructor(arg) {
super(arg);

this.#state = arg
}
get state() {
return this.#state
}
set state(newState) {
this.#state = newState
}

gimmeAThing() {
return `here's a thing`
}
goDoAThing() {
console.log('did a thing')
}
}

// When you can to this
const factory = (state) => ({
...baseFactory(),
state,
gimmeAThing: () => `here's a thing`,
goDoAThing: () => { console.log('did a thing') }
})

Prefer Composition over Inheritance

Like, I mean, everyone knows this one right? It’s famous. It’s the quote you drop to imply to others that you’ve actually read the Design Patterns book from the Gang of four, and it hasn’t just been sitting on your shelf for years and every now and then you tell yourself you should really sit down and read it because when you think about it, you’ve probably consumed more of it in quotes from articles you’ve read online than actually out of the book in your own library. Not me. But, you know… others 😳.

Composition is manifestly more versatile and flexible than inheritance. No serious software engineer that I know of questions the idea that one ought to prefer composition to inheritance.

So let me ask you this. How does one favor composition over inheritance with JavaScript classes? What does that even look like? JavaScript classes don’t have traits or mixins or anything like that to allow for composition. If you want to extend the functionality of a class in JavaScript, you need to subclass it. Composition is not really an option with JavaScript classes.

So if composition is out from the start, why are we using classes?!?!

I suspect part of the reason is an element of an appeal to novelty fallacy. Classes are a newer feature of the language and therefore must be superior to preexisting ones. But perhaps even more than that, there’s also an incorrect assumption that classes are more versatile or offer better ergonomics than objects. What with their methods, getters, setters, private members, static members, constructors, and what not, aren’t classes better than what we were doing before with function constructors and closures?

Well, inasmuch as we were using older JavaScript features to emulate classes, yeah. Classes are better. But what if we never should have been trying to emulate classes to begin with?

Look, there are legitimate reasons to use classes in JavaScript… probably. Or at least I’m willing to concede that I haven’t experienced the entire universe of problems to solve, and it seems plausible that for at least some of those, classes are a better solution. It’s just… apart from dealing with the DOM (where one is compelled to deal with classes because that’s what the spec authors semi-arbitrarily decided), I haven’t run into any of them yet. In every real world case I’ve come across, plain objects are at least as versatile and ergonomic as classes, and most of the time more preferable. In fact, it seems to me like classes are just Rube Goldberg machines for objects. There’s just a whole lot more concepts and ceremony that go in to classes just to do everything we can already do more simply without them. Don’t believe me? Let’s look as some examples.

Constructing a object of a given shape

Ostensibly, this is the classic case. You need an object? Use a class. Let’s take a look.

Class pattern

class BeautifulObjectClass {
isMagnificent = true

constructor(marvelousness = 11) {
this.marvelousness = marvelousness
}
}
const myBeautifulObject = new BeautifulObjectClass()
myBeautifulObject.isMagnificent // true
myBeautifulObject.marvelousness // 11

Delightful. Could couldn’t be simpler. Could it? Well, actually, yeah!

Factory pattern

const beautifulObjectFactory = (marvelousness = 11) => ({
isMagnificent: true,
marvelousness
})
const myBeautifulObject = beautifulObjectFactory()
myBeautifulObject.isMagnificent // true
myBeautifulObject.marvelousness // 11

Shorter, clearer, more signal, less noise, fewer things to think about. This is just a function that returns an object. That’s all. But it’s called the factory pattern. And it can do anything a class can.

Methods? sure.

Class pattern

class BedazzlerClass {
dazzle() {
console.log('Look at how dazzling I am!')
}
}
const bedazzled = new BedazzlerClass()
bedazzled.dazzle() // Look at how dazzling I am!

Factory pattern

const bedazzlerFactory = () => ({
dazzle() {
console.log('Look at how dazzling I am!')
}
})
const bedazzled = bedazzlerFactory()
bedazzled.dazzle() // Look at how dazzling I am!

Private Members? Come on son!

Class pattern

class Shhh {
_secrete = ''

constructor(secrete) {
this._secrete = secrete
}

revealYourSecretes: () => this._secrete

}
const shh = new Shhh('Mischief managed')
shh.revealYourSecretes() // 'Mischief managed'
shh._secrete // 'Mischief managed' 🤥
shh._secrete = 'password123' // 🤭
shh._secrete // 😱

Did you catch that? With the class, you have this “private by convention” problem where you have to store your value in a property that is public, but we let users know that we want them to pretend its private by starting the name with an “_”. That’s not very safe. But it’s cool, JavaScript classes just got a way to have truly private members!

class Shhh {
#secrete = ''

constructor(secrete) {
this.#secrete = secrete
}

revealYourSecretes: () => this.#secrete

}
const shh = new Shhh('Mischief managed')
shh.revealYourSecretes() // 'Mischief managed'
shh.#secrete // Uncaught SyntaxError: Private field '#name' must be declared in an enclosing class 😅

So that’s nice. Certainly an improvement, but we’ve now added yet another thing to remember. The more things to remember, the more footprint there is for human error. Simpler is better. Especially if you don’t loose any functionality. Now let’s look at the factory pattern solution.

Factory pattern

const shhhFactory = (secrete = '') => ({
revealYourSecretes: () => secrete
})
const shh = shhhFactory('Mischief managed')
shh.revealYourSecretes() // 'Mischief managed'
shh.secrete = 'password123'
shh.revealYourSecretes() // 'Mischief managed' 🙌

Getters/Setters? Yep.

Class pattern

class NameManagerClass {  constructor (name = '') {
this.#name = name
}
get name() {
return this.#name
}

set name(newName) {
this.#name = newName
}
}const nameManager = new NameManagerClass()
nameManager.name // ''
nameManager.name = 'Rumplestiltskin'
nameManager.name // 'Rumplestiltskin'

Factory pattern

const nameManagerFactory = (name = '') => ({

get name() {
return name
},

set name(newName) {
name = newName
}
}const nameManager = new NameManager()
nameManager.name // ''
nameManager.name = 'Rumplestiltskin'
nameManager.name // 'Rumplestiltskin'

Composition

In the context of objects, composition is the idea that one object can be composed of all the parts of two or more other objects. Let’s see what this looks like with classes.

Class pattern

class AClass {
propA: 'propA'
}
class BClass {
propB: 'propB'
}
class CompositeClass {
// 🤷
}

There’s no canonical way to do composition with classes in JavaScript that I am aware of. It’s programming though, right. I’m sure we can hack something together…

class APrimeClass {
propAPrime = 'propAPrime'
}
class AClass extends APrimeClass {
propA = 'propA'
}
class BClass {
propB = 'propB'
}
class CompositeClass {
constructor() {
const a = new AClass()
const b = new BClass()

const ab = {
...a,
...b
}
Object.entries(ab).forEach(([key, val]) => {
this[key] = val
})
}
}
new CompositeClass()
// { propAPrime: 'propAPrime', propA: 'propA', propB: 'propB' }

That’s about the easiest way I can think of to do it. It even works if one class inherits from another (though we all know we should avoid that right?). But There’s a lot going on there. It’s not axiomatic. There are rough edges. What’s the factory situation look like?

Factory pattern

const aFactory = () => ({
propA: 'propA'
})
const bFactory = () => ({
propB: 'propB'
})
const compositeFactory = () => ({
...aFactory(),
...bFactory()
})
compositeFactory()
// { propA: 'propA', propB: 'propB' }

Well, damn. That was easy.

Oh, but wait! You said with classes, it can still work with inheritance. So even if it’s something to be avoided, it’s still something we can’t do with plain ol’ objects… is it?

— Some nay-saying, eagle-eyed reader:

Hold my Diet Dr. Pepper…

const aPrimeFactory = () => ({
propAPrime: 'propAPrime'
})

const aFactory = () => ({
...aPrimeFactory(),
propA: 'propA'
})
const bFactory = () => ({
propB: 'propB'
})
const compositeFactory = () => ({
...aFactory(),
...bFactory()
})
compositeFactory()
// { propAPrime: 'propAPrime', propA: 'propA', propB: 'propB' }

Now some of you be like:

Oh snap indeed. (Image source)

Others be like:

That’s not inheritance, that’s just more composition. (Image source)

Well, ok, yeah. I just used more composition. But, uh, that’s what inheritance kinda is. Functionally, it’s just linear composition.

Ok, cheap trick, I’ll admit it. But, for the sake of completeness, I will show you real, proper inheritance with plain objects and the factory pattern. Just, please don’t use it. The whole point is to avoid inheritance. There is always a better way.

// Never do this!!const superObjectFactory = () => ({
superProp: 'superProp'
})
const grossInheritanceFactory = () => {
const obj = ({
subProp: 'subProp'
})

obj.__proto__ = superObjectFactory()
return obj
}

Ugh! I feel so icky! 🤢

Type checking

One last thing that we need to cover. And when you get right down to the bones of it all, this is really the reason for classes in JavaScript. It’s not that they enable any functionality that was missing before. It’s that they enable a particular kind of pattern in programing. Type checking. The crux of it is, everything I’ve shown you so far is oriented around the idea that an object has a property or method or whatever. We call this relationship between objects and their purpose a has-a relationship. It is fundamentally functionality oriented.

But class based programming is oriented around the idea that an object is a certain type. We call this relationship between an object and its purpose an is-a relationship. It is fundamentally identity oriented.

There is actually some monumental paradigm shifts between these two ways of thinking. The most obvious of which is a code base obsessed with type checking and error throwing:

if (obj instanceof SomeClass ) {
// do something I know I can do with SomeClass
} else {
//Crap! I don't know what to do! Uhhhhh
throw new Error('obj is not and instance of SomeClass')
}

Now me, personally, I find this kind of programming grotesque and overly complicated. Raising errors — like other forms of side effects — should be avoided all together or pushed to the edges of the system as much as possible. Littering a code base with exceptions, error throwing, and type and instance checks makes everything difficult to follow and full of landmines for your users. Failing loud and fast is great for development, but it doesn’t help your users out at all, and it’s a nightmare to read through (really, any branching logic makes comprehending the code more difficult, but that’s for another article). But this is what modern object-oriented programing looks like, so you are almost certain to run into it if not actually be trained to write it.

So plain objects with the factory pattern does not allow for instance checks, and that’s a virtue. There are many, many patterns that avoid the issues that class oriented programming create and for which class oriented programming have created dozens and dozens of concepts to correct for them. I think the more you have to think about with respect to your design patterns, the less you’re thinking about the problem at hand. Using plain functions and objects in JavaScript really simplifies things, and allows you to focus on the actual problem you’re trying to solve.

Just for kicks, let’s put every good feature that classes offer together in one class, and compare it to what that would look like with the factory pattern

Class pattern

class SuperClass {
superProp = '🦸‍♀️';

constructor(name) {
this.name = name;
};
someSuperMethod() {
console.log(`I'm ${this.name}!`)
}

someOverrideMethod() {
console.log(`I get overridden`)
}
}
class FinalClass extends SuperClass {
finalProp = '🪦';

#privateProp = '🔐';
#configProp = null;

constructor(configProp) {
super('super');
this.#configProp = configProp;
};

someMethod() {
console.log(`I'm only a member of FinalClass`);
};

someOverrideMethod() {
console.log(`I override the super class method of the same name`);
};

get finalProp() {
return this.#privateProp;
};

get configProp() {
return this.#configProp;
};

set configProp(newVal) {
this.#configProp = newVal;
};

}

Factory pattern

const superFactory = (name) => ({
superProp: '🦸‍♀️',

someSuperMethod() {
console.log(`I'm ${name}!`)
},

someOverrideMethod() {
console.log(`I get overridden`)
}
});
const finalFactory = (configProp) => {
const privateProp = '🔐';

return {
...superFactory('super'),
finalProp: '🪦',

someMethod() {
console.log(`I'm only a member of the finalFactory object`);
},

someOverrideMethod() {
console.log(`I override the method of the same name in superFactory`)
},

get finalProp() {
return privateProp;
},

get configProp() {
return configProp;
},

set configProp(newVal) {
configProp = newVal;
}
}
}

I think the most important thing to recognize in these two examples is how completely normal the factory function is. There’s no unusual syntax there. There’s nothing there you wouldn’t probably write hundreds of times in your every day work. It’s just a regular function. It’s possible you might not be as familiar with the getter/setter on the object, but those predate classes. JavaScript classes didn’t need to do anything different to support that. The short of it is, a function that returns an object is just far simpler than — and can do just as much as — classes. Why not prefer the simpler case. At least then you can have composition. Something you can’t get from JavaScript classes.

Further & recommended reading

Design Patterns: Elements of Reusable Object Oriented Software by Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides

The Hidden Treasures of Object Composition by Eric Elliott

JavaScript Factory Functions with ES6+ by Eric Elliott

Elegant Error Handling by JR Sinclair

Class Composition in JavaScript by Alex Jover Morales — overall a terrific article but in his zeal to demonstrate how one can do composition through JavaScript classes, the author inadvertently demonstrates the lengths one has to go through to sort of look like you’re doing it, but actually it’s just really, REALLY deep inheritance bastardized to look like composition.

--

--

Cory
Geek Culture

Front End Engineer with deep technical expertise in JS, CSS, React, Frontend Architecture, and lots more. I lean functional, but I focus on maintainability.