Case Study: The Dark Side of Inheritance

Nick Iorio
Plated Engineering & Data Science
4 min readDec 17, 2018
Inheritance can get complicated quickly

In life, when we talk about inheriting things from our parents and grandparents, we’re often talking about attributes and behaviors, both good and bad. It’s much the same in programming. When we talk about inheritance in code, we’re talking about the behavior of classes. Subclasses inherit the methods and properties of their parent, or base, class. In theory, this allows us to avoid repetition, and give structure to our code. When we start using inheritance excessively or incorrectly, however, these benefits are quickly outweighed by the costs. In practice, we should use inheritance sparingly. It’s easy to get into trouble as we use inheritance more and more, creating inheritance chains where an object’s behavior is scattered over numerous classes.

The problem

A few months after starting at Plated, my team was tasked with restructuring our site’s sign-up funnel. This essentially just involved changing some existing steps and adding a single new step to the funnel. As I started diving into the code, I was filled with feelings of uncertainty and dread. Below is a diagram of just some of the javascript classes involved with our existing sign-up funnel:

Now imagine all these classes interacting with each other. Yikes.

And keep in mind, this is only showing one type of input (email) in one of the sign-up steps (email and delivery address). As you might imagine, it was very difficult to reason with. Form validation was scattered over various classes. The interactions between the classes that define funnel manager, form step, and form input were practically inscrutable. A project that should have produced visible results in a few days instead ground on for weeks in an unusable state.

The reasoning for such extensive use of inheritance was surely noble. Inheritance can be used to avoid repeated code: functions can live in parent classes and belong to child classes that are distinct but require similar functionality. But this kind of inheritance obfuscates more than it simplifies. In the example here, functionality was scattered over far too many classes, making difficult to locate or add functionality (what belongs to a FormNode but not a FormField? Seriously, please let me know if you have the answer). The fact that the inheritance chains shown here interact with each other in complex ways only multiplies the confusion.

Not only was the code difficult to comprehend, it’s terrifying to change. I wouldn’t want to change any existing methods on FormNode for fear of breaking every single input on our site. Scary code is code that is left untouched, poorly maintained, and poorly understood. While classes high up in the inheritance chain were scary due to their power over unseen descendants, classes lower in the chain were inscrutable and mystifying because most of its their behavior actually lived in superclasses above them on the chain. How can I begin to change the behavior of an EmailInput without understanding what a FormNode actually does?

The Solution

Not a single class in our new project extends more than one level past the base React component

The obstacles presented by this structure were bedeviling both to relatively new engineers like myself and to the most senior engineer on our team. Ultimately we decided to rebuild our signup funnel entirely. That decision had its own pros and cons that are the subject for another blog post. One thing we were certain not to do, however, was use deep inheritance chains. Not a single class in our new project extends more than one level past the base React component. In fact, much of the code eschews ES6 classes entirely – right now we rely on extending the base React component for lifecycle methods, but even that could be removed with the introduction of hooks.

Take SubscriptionEmailAndAddressStep above: It inherits from 3 custom classes and React.Component. Its equivalent in the new project is AddressStep, which only extends that same base React component, a well-documented part of the React framework. There’s no superclasses in our code to dig through. Mind you, this simplification wouldn’t be worth much if it added lots of duplication. After all, the other funnel steps (plan selection, billing, etc) have a lot of the same behavior: they all have forms with inputs and forward and back buttons. We wouldn’t want to repeat ourselves excessively. The answer is to favor composition over inheritance. Rather than using inheritance, we import components within a given class to achieve the desired functionality. In this case, we import various Form and Layout components that are repeated throughout the sign-up steps. Each one of these components comes from either a well-documented library, or a class or function that we have defined in a single file. No more traveling down the rabbit hole to find where behavior is defined. If we want a special kind of input, we write a new component that’s composed with a standard input, rather than subclassing. And when we change the behavior of a class, looking for knock-on effects is a single keyword search away.

Now, this is as deep as the rabbit hole goes.

The proof has been in the pudding: making changes and iterations to our new signup funnel is vastly easier and less complicated. When looking at a component, I feel confident that I can see and understand its behavior at a glance, rather than worry what behavior is lurking in some distant ancestor or might be altered in a remote descendant. Most importantly, that feeling of uncertainty and dread has disappeared.

--

--