A few months later you release your game and it’s an overnight success! Players love it, but after a while they become bored with just mages and fighters and they demand a new player type, ‘paladin’, that can cast spells and fight.
The Problems of Classical Inheritance
You look at your class structure and ponder how to implement the paladin. You consider a couple of options:
- You could write a new Paladin class that extends from Character and then copy the fight() and cast() code from Fighter and Mage, but then you’d be duplicating code and you know that’s not a good solution.
- You could move fight() and cast() code up into the Character class so that all three character types could use them. This is perhaps a better solution, but you’ll also need to override the fight() function in the Mage class and the cast() function in Fighter class.
Neither looks like a very good solution… and why are you having to muck about in code that’s working perfectly fine to add the Paladin?
You’ve just discovered some of the problems with classical inheritance.
- You have to define your class taxonomy in advance. The is just about impossible to get right the first time, except for trivial projects.
- If you don’t get it right, you’ll be forced to change it later. And unfortunately, the parents and the children are tightly coupled which requires you to make changes in many places, and probably add bugs.
These problems are so common that there are names for them. Option 1 is known as the duplication by necessity problem. Option 2 is called 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, creator of Erlang
The problem isn’t really with object-oriented languages. It’s with classical inheritance, which makes you think in terms of what things are rather than what they do.
In this example, we focus on what an object can do rather than trying to define what it is. We define behaviors using factory functions that accept state and return an object that acts upon it.
Our canCast() function accepts state and returns an object with the cast() function. When we execute cast(‘fireball’) the function logs to the console and reduces the mana by 1. Note that we could have many functions on the object returned by canCast(), perhaps one for each spell, but it’s more future-safe to have only one.
We create our mage with another factory function. In this case, we define our state with some initial values, and then we use Object.assign() to create and return a new object that includes the properties from state and also the cast() function. This copying of properties from one or more objects into a new one is known as concatenative inheritance.
Our brand new paladin can be created like this:
Let the gamers rejoice!
`new` and `this`
You may have noticed one difference between object composition and classical inheritance that I haven’t mentioned. In classical inheritance, you create a new object using the
new keyword, but you don’t use
new with factory functions. This is another advantage that object composition has. When using classical inheritance, it is easy to forget the
Additionally, you probably noticed that the object composition approach completely avoided the use of
this keyword is confusing to many, and doesn’t behave quite the same way as it does in other languages, so avoiding it helps in code comprehension.
I should point out here that avoiding
this comes at a cost. Because Object.assign() copies the properties (including functions) from one object to another, you are increasing the memory burden. When you use prototype delegation and the
this keyword you don’t duplicate the functions and properties, you delegate instead. That’s the value of prototypal inheritance, but it doesn’t mean you have to emulate classes. I’ll be exploring this in a future article.
- In classical inheritance, we tend to think of our objects in terms of what they are, but when using object composition we think about what they can do.
- Classical inheritance is difficult to do correctly, and difficult to change later.
It may seem as though I’ve been bashing pretty hard on classical languages in my last couple of posts. If you’ve done much OOP you already know you should prefer object composition over inheritance, and many classical languages address this need through interfaces or modules. Unfortunately, we are nearly all taught about classes first and interfaces and object composition later. Why?
And to be fair, creating objects using constructor functions and prototype delegation is faster than factory functions, but the difference is so small that unless you are creating 10s of thousands of objects per second, it won’t have a significant impact.
I encourage you to play with the code above by copying it to repl.it
See if you can add an Archer to the party, or modify the behaviors so that they can’t be used too often!
Also, please check out my other posts: