Part 1: How to Escape the 7th Circle of Hell
Before we get into this, allow me to introduce myself — you’re probably going to wonder who I think I am before this is over.
I have contributed to software experiences for Adobe Systems, Zumba Fitness, The Wall Street Journal, ESPN, BBC, and top recording artists including Usher, Frank Ocean, Metallica, and many more.
Once Upon a Time
I was trapped in the darkness. I was blind — shuffling about, bumping into things, breaking things, and generally making an unholy mess of everything I touched.
In the 90's, I was programming in C++, Delphi, and Java and writing 3D plugins for the software suite that eventually became Maya (used by lots of major motion picture studios to make summer blockbuster movies).
- Prototypal Inheritance (objects without classes, and prototype delegation, aka OLOO — Objects Linking to Other Objects), and
- Functional Programming (enabled by lambdas with closure)
If you haven’t yet, it’s time to level up.
You’re coding in this amazing, game-changing, seminal programming language and completely missing what makes it so cool and interesting.
We’re Constructing a Mess.
“Those who are not aware they are walking in darkness will never seek the light.” ~ Bruce Lee
Constructors violate the open/closed principle because they couple all callers to the details of how your object gets instantiated. Making an HTML5 game? Want to change from new object instances to use object pools so you can recycle objects and stop the garbage collector from trashing your frame rate? Too bad. You’ll either break all the callers, or you’ll end up with a hobbled factory function.
If you return an arbitrary object from a constructor function, it will break your prototype links, and the `this` keyword will no longer be bound to the new object instance in the constructor. It’s also less flexible than a real factory function because you can’t use `this` at all in the factory; it just gets thrown away.
Constructors that aren’t running in strict mode can be downright dangerous, too. If a caller forgets `new` and you’re not using strict mode or ES6 classes [sigh], anything you assign to `this` will pollute the global namespace. That’s ugly.
Prior to strict mode, this language glitch caused hard-to-find bugs at two different startups I worked for, during critical growth periods when we didn’t have a lot of extra time to chase down hard-to-find bugs.
Welcome to the Seventh Circle of Hell.
“Quite frequently I am not so miserable as it would be wise to be.” ~ T.H. White
Everyone has heard the boiling frog analogy: If you put a frog in boiling water, it will jump out. If you put the frog in cool water and gradually increase the heat, the frog will boil to death because it doesn’t sense the danger. In this story, we are the frogs.
If constructor behavior is the frying pan, classical inheritance isn’t the fire; it’s the fire from Dante’s seventh circle of hell.
The Gorilla / Banana problem:
“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
Classical Inheritance generally lets you inherit only from a single ancestor, forcing you into awkward taxonomies. I say awkward because without fail, every OO design taxonomy I have ever seen in a large application was eventually wrong.
Say you start with two classes: Tool and Weapon. You’ve already screwed up — You can’t make the game “Clue.”
The Tight Coupling Problem
The coupling between a child class and its parent is the tightest form of coupling in OO design. That’s the opposite of reusable, modular code.
Making small changes to a class creates rippling side-effects that break things that should be completely unrelated.
The Duplication by Necessity Problem
The obvious solution to taxonomy troubles is to go back in time, build up new classes with subtle differences by changing up what inherits from what — but it’s too tightly coupled to properly extract and refactor. You end up duplicating code instead of reusing it. You violate the DRY principle (Don’t Repeat Yourself).
As a consequence, you keep growing your subtly different jungle of classes, and as you add inheritance levels, your classes get more and more arthritic and brittle. When you find a bug, you don’t fix it in one place. You fix it everywhere.
“Oops. Missed one.” — Every Classical OO programmer, ever.
This is known as the duplication by necessity problem in OO design circles.
ES6 classes don’t fix any of these problems. ES6 makes them worse, because these bad ideas will be officially blessed by the spec, and written about in a thousand books and blog posts.
P.S. Don’t use `super` unless you enjoy stepping through the debugger into multiple layers of inheritance abstraction.
These problems have a multiplying effect as your application grows, and eventually, the only solution is to rewrite the application from scratch or scrap it entirely — sometimes the business just needs to cut its losses.
I have seen this process play out again, and again, job after job, project after project. Will we ever learn?
At one company I worked for, it caused a software release date to slip by an entire year for a rewrite. I believe in updates, not rewrites. At another company I consulted for, it almost caused the entire company to crash and burn.
These problems are not just a matter of taste or style. This choice can make or break your product.
Large companies can usually chug along like nothing is wrong, but startups can’t afford to spin their wheels on problems like these while they’re struggling to find their product/market fit on a limited runway.
I’ve never seen any of the problems above in a modern code base that avoids classical inheritance altogether.
Step into the light.
“Perfection is reached not when there is nothing more to add, but when there is nothing more to subtract.” ~ Antoine de Saint-Exupéry
There is a small, but steadily growing community of developers whose coding styles have been transformed by stamps. Stampit is in production use in multiple apps with millions of monthly active users.
“I have been using Stampit a lot and really enjoy the power afforded by the simplicity in the separation of concerns between the types of prototypes as well as composable nature of stamps. Classical deep inheritance trees always bothered me, especially in the context of web development where business needs are often a moving target. The above mentioned talk and Stampit have inspired different thinking and allow me to do things that feel like interfaces without the overhead, or inherit private data with privileged methods from multiple sources. My prototypal inheritance is now strong, my objects have become shapeless, formless, like water. Keep up the great work, class sugar has nothing on this.” ~ Justin Schroeder, Senior Developer, Wrecking Ball Media Group
Stampit isn’t the only alternative, of course. Douglas Crockford doesn’t use `new` or `this` at all, instead opting for an entirely functional approach to code reuse.
All his objects are just stateless bags of functions, or data-only objects like associative arrays with no methods. This works well unless you’re creating a hundreds of thousands of objects and you need your app to perform smoothly at or near realtime (think game engines, realtime signal processors, etc…). In those situations, delegating calls to methods can save you from a lot of manual memory management.
The copy mechanism is another form of prototypal inheritance. Sources of clone properties are a specific kind of prototype called exemplar prototypes, and cloning an exemplar prototype is known as concatenative inheritance.
When I tell people that constructors and classical inheritance are bad, they get defensive. I’m not attacking you. I’m trying to help you.
People get attached to their programming style as if their coding style is how they express themselves. Nonsense.
What you make with your code is how you express yourself.
How it’s implemented doesn’t matter at all unless it’s implemented poorly.
The only thing that matters in software development is that your users love the software.
I can warn you that there’s a cliff ahead, but some people don’t believe there is danger until they experience it first hand. Don’t make that mistake; the cost can be enormous. This is your chance to learn from the mistakes that countless others have made again and again over the span of decades. Entire books have been written about these problems.
The seminal “Design Patterns” book by the Gang of Four is built around two foundational principles:
“Program to an interface, not an implementation,” and “favor object composition over class inheritance.”
Because child classes code to the implementation of the parent class, the second principle follows from the first, but it’s useful to spell it out.
The seminal work on classical OO design is anti-class inheritance.
It contains a whole section of object creational patterns that exist solely to work around the limitations of constructors and class inheritance.
Even James Gosling, the creator of Java, admits that Java didn’t get objects right.
Kyle argues that prototypal inheritance is anti-class, that’s simpler and better than class. He even coined the term OLOO (Objects Linked to Other Objects) to clarify the distinction between prototype delegation and class inheritance.
Good code is simple.
“Simplicity is about subtracting the obvious and adding the meaningful.” ~ John Maeda
- Gets simpler (Easier to read and to write. No more wrong design taxonomies.)
- Gets more flexible (Switch from new instances to recycling object pools or proxies? No problem.)
- Gets more powerful & expressive (Inherit from multiple ancestors? Inherit private state? No problem.)
The Better Option
“If a feature is sometimes dangerous, and there is a better option, then always use the better option.” ~ Douglas Crockford
I’m not trying to take a useful tool away from you. I’m warning you that what you think is a tool is actually a foot-gun. In the case of constructors and classes, there are several better options.
Another common argument that programmers use is that it should be up to them how they express themselves, as if code style rises to the level of art or fashion. This argument is a purely emotional and irrational:
Your code isn’t the product of your self expression any more than a painter’s paintbrush is the product of their self expression. Code is the tool. The program is the product.
Yes, some code is art in and of itself, but if it doesn’t stand alone published on paper, your code doesn’t fall into that category. Otherwise, as far as your users are concerned, the code is a black box, and what they enjoy is the program.
Good programming style requires that when you’re presented with a choice that’s elegant, simple, and flexible, or another choice that’s complex, awkward, and restricting, you choose the former. I know it’s popular to be open minded about language features, but there is a right way and a wrong way.
Choose the right way.
~ Eric Elliott