w0rpzone

Blog

Why I No Longer Believe in Inheritance

Polymorphism is a notion that comes from type theory. Polymorphic types yield a useful property I'm sure we all desire in programming. This is the ability to write functions conforming to particular types, such that the function can theoretically be used with any compatible type.

Over the last few decades, one very popular way of achieving this effect has been to use class inheritance. A class is defined which implements some behaviour, and then another class can be defined which inherits that original class and adopts its behaviour, making adjustments where neeeded.

This has been one of the worst mistakes in the design of modern programming languages. Every modern university will at some level teach object oriented programming with respect to inheritance. This is to say the every modern university is teaching students a bad way to write computer programs. This needs to stop.

So why do I feel this way? Here are my reasons.

Inheritance Makes Code Hard to Follow

Let's say you have a type in Java, defined like so.

abstract class Animal {
    abstract String speak();
}

This is the simplest possible example, you have a class, it has a single method which performs an action. You can write a subclass to implement the behaviour.

class Dog extends Animal {
    @Override
    String speak() {
        return "bark";
    }
}

Great. Animals all speak, dogs bark when they speak. So suppose you have an Animal, and I pose you a question.

To understand what .speak() will do, where do you have to look?

I do not think this is a lot to ask. All I'm asking is that you can look at a line of code with one method for one type, and tell me what it's going to do. The problem is that you have absolutely no idea what the method will do, without flattening your hierarchy and finding out exactly which type is being used. Think of it another way.

How many subtypes of Animal exist?

The first question has led to another question, how many types of animal are there? The answer is that you cannot know the answer. There are potentially an infinite number of Animals out there. You can write as many subclasses as you like.

So the conclusion is that it's not possible for you to understand what your code does. You have specified a few names and types, but really it could do anything. Outside of testing, how in the world can you ever hope that programs written this way do anything close to what you expect at all? It's a system of faith.

Procedures Are Easier to Understand and Adapt than Methods

So methods which can do potentially anything are hard to understand, so what happens when we have a whole bunch of them? Let's look at a real world modern example in Django.

Let's write a page with a form using the generic FormView in Django with Python 3. I will omit import lines, as they aren't the important parts.

class ApplicationFormView(FormView):
    template_name = "application_form.html"
    form_class = ApplicationFormView
    success_url = "/application-thank-you/"

    def form_valid(self, form):
        form.send_email()

        return super().form_valid(form)

This is almost exactly what the Django documentation recommends. I effectively ripped off what's in the documentation and changed the names a little. So what does the view do? It takes some POST data, does validation on it with the Form class, and sends off an email and goes to a thank you page if the form is valid. That's a lot of behaviour there for not a whole lot of code. It seems like we're saving a lot of time!

Later, your manager comes to you and asks you to show three different forms for three different regions, and send three different emails. This is not a contrived example. I have been asked to do exactly this many times in my current job. Now your head is spinning thinking of how to fit those requirements into your ideal structure. Maybe you can introduce a method which returns a different form class based on some information taken from the request, so you write in a get_form_class method into FormView so you can make this happen, or you override a FormView method to call this other method for you at some point, maybe in your new super class. Sending your different emails is easy! You just do push up refactoring on your Form class, which is now abstract, and implement 'send_email' differently so it uses different templates.

What a bother. Silly manager, right? Asking for more features which break up your perfect encapsulation of behaviour in your polymorphic type system. No, wrong. Silly you for relying on this OOP inheritance horseshit. Imagine if you wrote things the procedural way.

def send_email(form):
    ...

def application_form_view(request):
    form = ApplicationForm(request.POST or None)

    if form.is_valid():
         send_email(form)

         return HttpResponseRedirect("/application-thank-you/")

    return render(request, "application_form.html", {
        "form": form,
    })

First, I want you to make one important observation. Compare the lines of code between this function and the class above? There really isn't a whole lot of difference between them in terms of code size. But notice one other thing. You now see everything important the inheritance hierarchy of FormView classes did before, only all at once, in one straight line from start to finish. You get some objects, you create some more objects, you work with the objects, you produce a result. So let's come back to the change in requirements which we experienced before, and by the way, adapting to changing requirements if you are a programmer is the vast majority of your job.

def send_european_email(form):
    ...

def send_american_email(form):
    ...

def send_asian_email(form):
    ...

def form_type_for_region(region):
    if region == "europe":
        return EuropeoanApplicationForm
    elif region == "america":
        return AmericanApplicationForm
    elif region == "asia":
        return AsianApplicationForm

    assert False, "Invalid region: " + region

def email_function_for_region(region):
    if region == "europe":
        return send_european_email
    elif region == "america":
        return send_american_email
    elif region == "asia":
        return send_asian_email

    assert False, "Invalid region: " + region

def application_form_view(request, region):
    form_type = form_type_for_region(region)
    form = form_type(request.POST or None)

    if form.is_valid():
         send_email = email_function_for_region(region)
         send_email(form)

         return HttpResponseRedirect("/application-thank-you/")

    return render(request, "application_form.html", {
        "form": form,
    })

I didn't write the above example in a clever way on purpose, because with the procedural version of the view, you don't need to be clever to understand how to implement the change in requirements. You just go ahead and do it. Perhaps you can imagine ways of doing the above with less code? Perhaps you might use a dictionary mapping regions to form types. The important distinction worth noting is that it's easier to reason about changing behaviour when you write procedures instead of methods on classes.

This is what matters for your code, that you can write replaceable code, not extenstible code. Code you can replace shifts according to the needs of the day. Code you can extend grows outwards like a wart. The former is evolutionary. The latter is cancer.

I could apply the same ideas discussed here to the Form classes used above also, but that would take more time to discuss and I'd be hitting on exactly the same points again, so I won't bother.

Inheritance Makes Code Slow by Definition

Let us now consider what we might call "lower level" concerns. Speed. Making what code do what it already does as fast as it possibly can. If we want to think of speed in our programs, there are really only two measures of speed.

  1. Runtime analysis, Big O, growth rates, order of magnitude differences in speed.
  2. Constant time factors, the expense of doing a single thing, CPU behaviour.

So let's assume we are good algorithm designers, and we have thought everything we need to think about in terms of Big O analysis. We have done everything we must theoretically do to make our algorithms faster. Yet this code is still much faster than that code? Why? Is this some magical property of different languages? Are our "higher level" languages just not omitting the right CPU instructions? Can't we just fix that so they do?

If you are using OOP classes with inheritance, objects differing in size, virtual methods, and so on, the answer is plainly no. The closest you can dream to coming to making this code run at full speed is if you have a JIT compiler which optimises calls quickly at runtime, and you should already be able to imagine why this won't be able to compete with code which does the fast thing already.

So what is this property that makes some code fast and some code slow? It comes down to how objects end up being represented in memory. Picture a list of objects in memory. These objects use classes with the inheritance model, so they must be references to data, or pointers, and not the data itself. This is because the data varies in size. So you have one layer of indirection. Next, when you call a method on an object, you have to do a lookup for the method, if that method is virtual, at runtime. So you have another layer of indirection. So looking at some Java code...

List<Animal> animalList = new ArrayList<>();

/* ... */

for (Animal animal: animalList) {
    /* Dereference to get data -> method lookup -> return result */
    System.out.println(animal.speak());
}

So our code above keeps jumping around in memory. Why is this a problem? Because CPUs are optimised for doing things in a predictable order. CPUs have several levels of cache and predicition based around your code doing things in some obvious way. So you go in one direction in memory, you end up going through certain conditionals, generally it follows some kind of predictable pattern. More than that, the smaller your data structures are, the better, because then you can fit your data into smaller CPU caches, and the smaller a cache size you fit in, the closer to your CPU you get. Let us consider the same above, only in C++ where we do not use inheritance.

vector<Dog> dog_list;

/* ... */

for (auto &dog: dog_list) {
    /* Return result */
    cout << dog.speak() << endl;
}

The vector of Dogs is laid out in memory without indirection through pointers. If we step one element forward in memory, we will get the data for the next dog object, right there. For the methods, we know exactly which method to call ahead of time, so we don't need to find the right method at runtime. This means the CPU can cache the next set of dogs before we access them. This does wonders for program speed.

Deciding which behaviour to execute at runtime always makes programs slower. Every time. If you can make better decisions about what needs to be done statically, you can create more efficient programs. This is an asbolute truth. Class inheritance leads to slower programs.

Parametric Polymorphism via Static Duck Typing Does It Better

So we know of a number of problems with inheritance, but there must be some reason why we were after it in the first place? We wanted reusable code. We wanted to save time. That was our goal. So we wanted to be in the business of writing code hopefully once, and letting it work for all similar things. So is there another way?

It turns out there is another way to write polymorphic code, and it also turns out that it is more powerful and more useful than inheritance. I have taking to calling it "statically checked duck typing." Considering only imperative programming langauges for the moment, I know of only two programming languages that let you do this, C++ and D. The latter lets you do it with less headaches, both don't currently let you do it easily enough, in my opinion.

Because it's just easier to understand in D, we'll write an example in D. To write functions with static duck typing in D, we need to combine two things. First, we need template functions, functions which can work with potentially any type. Second, we need to use template constraints, so that functions are checked at compile-time to make sure that the types they are receiving are compatible, and hopefully report a helpful error message when functions are being called with incompatible types. Let's look at the filter function in the D standard library.

// Straight out of the current version of std.algorithm 
template filter(alias pred) if (is(typeof(unaryFun!pred)))
{
    auto filter(Range)(Range rs) if (isInputRange!(Unqual!Range))
    {
        return FilterResult!(unaryFun!pred, Range)(rs);
    }
}

The return value is a structure which acts as range of values, which is all the values matching the filter. The implementation of this filter result is itself only in the neighbourhood of 50 lines, and it's just doing obvious things for ranges. The point is that the above filter function works for any type which conforms to an InputRange interface, and not in the Java OOP sense. As long as the type has the right properties, checked at compile time with isInputRange(T), then this function itself will work. It will take any predicate function of a compatiable type for filtering with.

You don't need to rewrite code to match the interface, you don't need to use wrappers and composition to put your object through filter. The most you could need to do to make a reasonably compatible type work as an InputRange if it isn't already is to write aliases to existing functions, or new functions which do the right things. This is true type polymorphism, because it doesn't force your types into hierarchies, it's bounded on the properties of objects, not superficial parent-child relationship information.

It also doesn't suffer nearly as much from the problems of not being able to understand code as overriding methods does, because once you know all of the types involved, you know exactly which functions are being called and where. You stop thinking about methods for data structures, and you start thinking about algorithms, which by the way happen to apply to types belonging to a particular category.

In the Future, Generic Functions Will Be Easier to Write

I have one major objection to template functions, and that is that there is some additional friction involved in writing the functions, because contraints on types cannot be expressed as if they were types themselves. There is a better way this code be expressed, and C++ is approaching this syntax with concepts. Let's modify the above function a little to use concepts, and a few more considerations from the compiler.

auto filter(UnaryFunction pred)(InputRange rs)
{
    return FilterResult!(unaryFun!pred)(rs);
}

This is the way things could potentially be in the future in D, supposing a number of improvements are made to the compiler. The most important of these is the additon of concepts, which look like types, but are actually more like template constraints which do not themselves name a type. My imaginary code here would expand into roughly what was specified before, only now it looks a lot cleaner and easier to understand, so easy in fact that it looks as good as any function using OOP inheritance you might have written before, perhaps better because it is more powerful.

This is where it's at. This is what I want to work towards. Code that is clean, exhibits obvious behaviour, and gives you a lot of return on investment. A little bit of code yielding a lot of use. That's the ideal, to write as little code as possible and to get as much out of it as possible. To write code that's obvious so it's easier to reason about when we get it wrong. Generic programming, when implemented in a programming language properly, gives us this power.

In Summary

In summary, inheritance in OOP isn't worth the trouble. Speaking pratically, you should only use it in a programming language when you are offered no other reasonable alternative. Modern languages provide you with many reasonable alternatives, which are surely cleaner and saner. Consider these alternatives when writing your next project.

Above all, don't hold our modern definition of OOP in high regard, which pertains mainly to OOP via classes and inheritance. Implementing new data structures is one of the foundations of programming. In the D programming language, I take great joy in knowing that I can use the 'struct' syntax to create new data structures, and provide properties on these structures that look like plain old attributes. This gives me the power to represent new types of data in my program which carry as much power as the built in primitives, which lets my imagination do what it should, run wild.

This idea of writing abstract data types shouldn't be confused with writing "extensible" classes with inheritance. There is no reason to take a project you have written using a series of functions and change it so your API now offers classes you can override. If anything, switching your project from a procedural programming paradigm to an OO with classes paradigm will make your project worse. Don't do it.

Post Your Comment

Need help with comment formatting?

Okay, here's a cheat sheet for you. Click here to get rid of this.

Choose a help topic below. Raw HTML is not allowed and will be replaced with ugly text. Other Markdown syntax is valid, but probably not relevant.

*italic* **bold**
_italic_ __bold__
1. Item #1
2. Item #2

* Unordered list item
* Unordered list item
Three backticks are your friend here: ```square = lambda x: x * x```

Also in a block like so:

```
def square(x):
  return x * x
```

Comments

Please stop writing highly subjective "Why I despise..." posts containing such bold, desperate claims, v.g. "This has been one of the worst mistakes in the design of modern programming languages". What are you? Some kind of prophet?

This is my website and I will continue to write incredibly subjective material. This website represents my own opinions. I'll probably write three more highly subjective articles just because it annoyed you.

Of course, feel free to write 3+ smartass iconoclastic posts from your macbook, but don't do it just for the sake of annoying me because chances I'll be reading them are pretty low. Your pretentiousness will surely annoy another people, though.

Given that you came back nearly two hours later to check if I replied, I expect you to be a religious reader of my blog. I'll keep pumping out the highly subjective articles, just for you.

I don't use Macintosh computers personally. It's nice to know that you're such an awful bigot that you would hate someone just because of the computer they use. I assume that you are such a horrible bastard that nobody would ever hire you for a job, and instead waste your time whining on random blogs on the Internet.

Nice article. Have you watched Brian Will's videos on YouTube about how OOP is bad in general? https://www.youtube.com/watch?v=QM1iUe6IofM