Class inheritance is broken by design

Or why you always struggle with those large class hierarchies

Your own hunch when you hit it

Maybe you've been wondering why code gets messy quite easily ones you start subclassing classes.

Did you mess it all up?

Do you wonder what's the right way in the end?

You think that the pros certainly know better than you... ? You are missing some piece of knowledge or experience to get it right, right?

You read somebody else's code with the huge class hierarchies and you are wondering how the hell you are supposed to make sense of it. Or how to correctly build on it. Loads of classes interconnected in intricate ways. You are jumping between files constantly, mentally straining and rying to make sense of the next useful change.

Maybe you are looking at a relatively simple project. For example a JavaScript game in the browser. It has a few objects at any time, one action that the player performs. Also a few states, like being in game and on the start menu. There is a score. It's open-sourced. You take a look at the source code and are dreaded with dismay - large classes, lots of fields in every of them, no clear responsibility of pieces of logic. What the hell is going on?! Is this the way programming is supposed to be?! πŸ₯Ή

Why we need to have this conversation

So, many beginner programmers have something like this as the agenda from a modern general purpose programming / software development course:

  1. Choose a language: e.g. C#, Java, JavaScript, Python (for example)
  2. Learn programmings basics: e.g. some basic theory, variables, loops, arrays, calculating and outputting basic stuff on the screen, running math logic (etc., haha)
  3. Learn OOP: e.g. often a pathetic attempt at teaching people that code should be structured in a particular class-based-OOP way
  4. -4-5-6... irrelevant to our conversation / <insert any further topics here>

Now, let me stress this one: one of the reasons a good part of fast-track initial programming education is focusing on OOP concepts like inheritance and polymorphism (in the context of classic inheritance) is because too much software exists, already using it and because it's a common mental model (trap) to fall into - that is, hyped by default as the correct way.

Then, beginner programmers start to think primarily in instances, coming from classes, which are beautifully structured into hierarchies of those classes. And that's default way to reuse code.

Ofcourse, if you were initially taught in this manner, how come you could think it's a bad choice?

The brutal truth

πŸ€• Classic OOP inheritance is bullshit. Complete and utter. It gets you in an unthinkablle mess right from the beginning. No way around it! If you try to do anything remotely more complex than something really simple, you will get either into the need of multiple software design patterns or into horrific mental conundrums on what's right to do next... or both, not focusing on your actual usecases while trying to untangle yourself from the bullshit you've already put yourself into. πŸ€’

If the above feels familiar to you - I have this question - how many times have you fell into the adversity of having to implement funtionalities of a sub-class just for the sake of fulfilling your hierarchy at 100%? How many times does a piece of code not make sense at all or there is an edge case which makes your hierarchy plain wrong?

If you are unfamiliar... just try building something complex. How far did you (manage to) go? What's the level of coupling (dependency between your objects) like? High? Very high? Yes.

The common rationale for using inheritance

People say it not once, but here it is, cited even on this Reddit thread): OO gives us a handy tool to model real world concepts in an elegant way.

I have 2 problems with such statements:

  1. class inheritance does not equal and is Not an insrinsic part of OOP (object orientated programming) - there are ways to still make use of and structure objects (without classic inheritance, e.g. without extend)
  2. code does not equal to the real world - like it or not - representaions of the real world should avoid being too real-wordly (or you inadvertently end up shooting yourself into the leg and creating the code equivalent of Frankenstein) - see the Banana - Gorilla - Jungle infamous OOP relationship example...

I will repeat it: classic OOP inheritance is bullshit. Complete and utter. 😭😭😭

Don't believe me?

A disclaimer: class or classic inheritance

That's what I mean: class inheritance is the same as classic inheritance... because there is also something called prototypal inheritance (funky, yeh? πŸ€”πŸ€‘)

But it's not available in many places (languages) - as a matter of fact JavaScript has that!

I will repeat it:

Class inheritance is bullshit

Unspeakable bullcrap which must be avoided at all cast - especially when you have better options! πŸ˜²πŸ€“πŸ€“

Don't believe me?

Java creator admits it

James Gosling, creator of Java admits he would leave the extend keyword out if he was to create Java again! A few thoughts on the subject: Why extends is evil

Gand of Four admits it

The so called GoF (authors of a very popular big book on software design patterns) say the following:

Favour composition over inheritance

Composition, if yo happen be a noob, is to contain other objects (has a relationship) rather than subclass objects (is a relationship). It's usually much cleaner. In the case of JavaScript, it's insanse to use classes as a main building block.

I will repeat it: class inheritance is bullshit. One more article from a random blog: Composition Over Inheritance

Speaking of JavaScript...

Douglas Crockford admits it

Douglas, JS's Mahatma (almost violently) preaches against usage of classes in JS!

Just one example, a quote from his precious article: Classical Inheritance in JavaScript! A small note: this article seems to be before the time of ES6's classes and super came into actual play (my bet on why Mr. Crockford is talking about implementing super... and then talking about how useless it is in JS hahahah πŸ’©):

I have been writing JavaScript for 14 years now, and I have never once found need to use an uber (super) function. The super idea is fairly important in the classical pattern, but it appears to be unnecessary in the prototypal and functional patterns. I now see my early attempts to support the classical model in JavaScript as a mistake.

Truth be told, after quite a few years of writing JavaScript professionally (and after having some grasp of functional programming concepts), I had the exact same realization (only my beard is still probably much shorter πŸ˜Άβ€πŸŒ« hahaha πŸ»β€β„οΈ)

You should admit it

<nothing here> Yes

Any solutions to the mess?

Try not to use class inheritance where possible. Just don't. Stop now, if you are doing so, the plain simple thing to do - use object composition, instead of inheritance when having to reuse functionality. That is, let object A contain B and C and not inherit from B and C.

Since this website is about JS / TS...

Make use of the better ways when in JavaScript - examples

JavaScript is extremely expressive.

With the next examples we will take the person-student relationship as an example on what we can do without class inheritance. To begin, in JS, you can create objects from other objects just like that:

const objA = {
a: 5
}

const objComposite = {
...objA,
b: 6
}

The next examples are in TypeScript.

Functions in JS are gods:

interface IPerson = { name: string, age: number }

const createPerson = (personConfig: IPerson) => {
return {
...personConfig
}
}

// note: this type of extends is allowed (we are dealing with interfaces only)
interface IStudent extends IPerson & { university: string }

const createStudent = (student: IStudent) => {
return {
...student
}
}

And functional programming (which is about transformations and grouping of data... and not only) - is extremely useful when writing any kind of JavaScript, due to the fact that functions can also be passed around like data to be used!

// building up on the above code...

const isStudent = (entity: (IStudent | IPerson)) => Boolean(entity.university)

const findStudents = (data: (IStudent | IPerson)[]) => data.filter(isStudent)

const peopleData: (IStudent | IPerson)[] = [{
name: 'John A',
university: 'C',
}, {
name: 'Jack B',
}, {
name: 'Mike C',
}, {
name: 'Ollie D',
university: 'C',
}]

const studentsOnly = findStudents(peopleData)
/**
* studentsOnly equals to
* [{
* name: 'John A',
* university: 'C',
* {
* name: 'Ollie D',
* university: 'C',
* }]
*/

What about turning an existing person into a student? Piece of cake! πŸ₯³

// building upon the previous code...

const makeStudent = (person: IPerson, studentConfig: Partial<IStudent>) => {
return {
...person,
...studentConfig
}
}

const uneducatedPerson = { name: 'Uneducated Person', age: '46' }
// now `educatedPerson` is a person
const educatedPerson = makeStudent(uneducatedPerson, { university: 'Some University Name' })

I am tempted to add one more example - attaching an action to a person (to individual instances of it, defaulting included):

// building upon the previous code...
const TPersonAction = 'introduceMyself' | 'mentionUniversity'

/** A `default` action we can make use of */
const introducePerson = (p: IPerson) => console.log(`Hello, my name is ${p.name}`)
/** A `default` action we can make use of */
const mentionUniversity = (p: IStudent) => console.log(isStudent(p)
? `I am studying at ${p.university}`
: `I am currently not studying at any unversity`)

const attachActionToPerson = (person: IPerson, actionName: TPersonAction, action: (p: IPerson) => void) => {
return {
...person,
[actionName]: action
} as IPerson & TPersonAction
}

const invokeActionOnPerson = (p: IPerson, actionName: TPersonAction) => {
return p[actionName] ? p[actionName]() : `Sorry, the ${actionName} is not defined for this person: ${p}`
}

const personWithAction = attachActionToPerson({ name: 'A Normal Person', age: 25 }, 'introduceMyself',
introducePerson)

const santa = attachActionToPerson({ name: 'Santa Claus', age: 256, university: 'Laplandia\'s University' }, 'introduceMyself',
(p) => 'Hooo hooo hooo! I am your favorite Santa Claus, little kids!')

attachActionToPerson(santa, 'mentionUniversity', mentionUniversity)

////////////// now let's try this whole thing:
invokeActionOnPerson(personWithAction, 'introduceMyself') // prints `Hello, my name is A Normal Person`

invokeActionOnPerson(personWithAction, 'mentionUniversity') // prints `Sorry, the mentionUniversity action is not...`

// after we do this:
attachActionToPerson(personWithAction, 'mentionUniversity', mentionUniversity)

// now this prints `I am currently not studying at any unversity`
invokeActionOnPerson(personWithAction, 'mentionUniversity')

invokeActionOnPerson(santa, 'introduceMyself') // prints `Hooo hooo hooo! I am your favorite Santa Claus, little kids!`
invokeActionOnPerson(santa, 'mentionUniversity') // prints `I am studying at Laplandia's University`

Did I use any kind of inheritance with the above examples? Actually yes - prototypal one. Not classical. But the more important thing? How many lines did it take to add that much logic with defaulting? Not that many! And we retain flexibility. That is: we've made use of pure functions to deal with our objects and we can always add or remove more similar functions just as easily! Please think about if there is any kind of evil coupling between the functions and the objects we create (or between the objects themselves) - you are right - there isn't. If we were to make use of class inheritance we would have started getting into very very awkward situations very easily. Such examples, however will not be part of this article.

A few tips when in JavaScript

  • don't inherit/subclass classes! At all - there are much nicer ways to do stuff: modules, pure functions, functional programming as a way to reuse and build up on existing code
  • I find this article and its spirit to be emblematic: The 2 pillars of JavaScript
    • if you don't understand everything in it, that's okay - it will come with the time when/if you use JavaScript long enough 🧠

There is a lot more on the subject of functional programming, JavaScript's ways and reusability. But let's leave it to other articles.

On thing, though, you could do right away is check out this little NPM lib - obs-disp, which does a very nice job with only functions and objects to build complete JavaScript apps and games with the least of effort...

Good luck!