(Not Only) OOP's Mistake: Hierarchy is not Abstraction
Table of Contents
In a recent talk on the “Return of Procedural Programming”, Richard Feldman made an interesting point. Talking about the problems of Object Oriented Programming (OOP), he noted that one of the pitfalls of this programming paradigm is inheritance, but that we can achieve the same goals without it.
The gist of this observation is the slogan “Composition over Inheritance”, but in this article, I want to point out a deeper issue: Creating an an abstraction is not creating an hierarchy. Abstraction and hierarchy are closely related, but still different concepts.
The mistake of OOP is to conflate them, and I think this is a very interesting mistake. In particular, because conflating abstraction with hierarchy is deeply engrained in our Western way of thinking.1 Basically, it is the idea that the ‘more abstract’ concept is ‘more essential’, because that’s what abstraction is - determining the essence of something, isn’t it? But it is not, and OOP is a good example to show why.
Modeling Abstraction in OOP #
So let’s first have a closer look at the problem. It is a peculiarity of OOP that it encourages strict (and sometimes quite deep) levels of inheritance, in which a so-called “base” object inherits its methods and data to another “subclass”. From there, we can continue the pattern and create yet another “sub-subclass”, and so forth.
These OOP hierarchies are intended to serve as a practical means of
abstraction: The “base class” unifies the behavior common to all
derived classes, yet gives them the freedom of specialization. To give
a well-known (and rather worn-down) example: We can build a base class
Animals
for entities that all make a noise, and then specify that a
dog barks while a cow moos.2
Given such an hierarchy, we can now address all kinds of animals in
the same way, namely, as Animals
. All derived (lower) classes
share the same behavior because they are all bound to the same
superclass.
In this way, you treat a variety of classes as if they were one and
the same kind, abstracting away from their differences. The programmer
can write code that makes any Animal
make a noise, whatever it is
exactly. Or, more realistically, they can connect to a database no
matter how it is implemented in detail.
This is a powerful paradigm. It frees us from the burden to deal with the particularities of each individual animal (or database engine) and allows us to address them in a uniform way.
It also resonates well with a long tradition of Western thought. There is no lack of philosophical systems which try to model the world as an hierarchy, where everything is assigned its proper place.3 And social order, too, is traditionally imagined in quite rigid, hierarchical, and orderly ways.
No wonder this paradigm became the industry standard: These mental habits are still highly influential today, and they structure many daily decisions. Thus it seems quite natural to us to believe that by creating an abstract base class, we are truly striving for the essence of what it means to be something of a certain kind (say, a database).
The Problem: Code Has to Change #
The problem with hierarchical abstraction is that code is never finished, and then hierarchy begins to stand in the way of change.
It is an important fact that computer programs, not unlike biological organisms, are constantly changing and need to adapt to new conditions and requirements. Thus, the need comes up to adjust the behavior of the program. Yet the hierarchical abstraction, fruitful at the beginning, quickly becomes a burden when confronted with the need for change.
More formally put: For an hierarchical model to work, the subclasses must not only adhere to the explicit contract, as defined by the methods and their parameters. They also must display the corresponding behavior at the implementation level. They are forced to behave in a certain way, simply by virtue of being at a certain place in this hierarchy; and this can become an obstacle for change.
This restriction is particularly noticeable in larger code bases, where it is not possible to change the hierarchy. The hierarchy can be so deeply interwoven with the whole program that changing it amounts to rewriting the whole program. In such a situation, we are forced to find creative ways around the limitations of the hierarchy.
Patching the Hierarchy #
Imagine, for example, that we are still modelling the behavior of animals. Now the need comes up to introduce an animal that remains silent simply because it makes no noise (say, a worm). Good idea, but: our base class did not anticipate this case.
In response to that challenge, one common pattern is to make the base
class more flexible. For example, we add an optional flag that allows
us to specifiy whether our animal actually makes a sound or not. By
setting this flag to the default value of noise
, we keep our
extended class hierarchy in line with the established behavior.
This looks like a great solution: We can keep everything as it is, except in those new cases where we need that new behavior. Every animal is still making noises, except those which are not.
But you can see where this leads. All animals are equal, but some animals are now less equal than others. From now on, we can no longer rely on every animal making a noise.
The simplicity of our interface has been lost. We can no longer address “all animals” with one single gesture, say, in a loop; we must be prepared to add exceptions, distinctions and special cases.
But this is exactly what the abstraction was made for: To be able to
abstract from all these differences, to be free to treat all Animals
equally. By patching the hierarchy, we lead it away from that purpose.
The whole idea of a clean abstraction falls apart, and we find
ourselves fighting against the very hierarchy that was introduced to
make our lives easier.
The Alternative: Tagging Types #
Everyone working with a larger OOP project has seen this patchy code, and has suffered from it. Which is why in the end, even a language like Java, the paradigm of OOP, has abstractions that do not rely on hierarchies. Java calls them ‘interfaces’; Rust has a similar concept called ‘traits’; and C++ has so-called ‘concepts’.
In contrast to the hierarchical model of inheritance, these abstract types leverage the power of tagging. By assigning an abstract type, only a very limited and specific contract needs to be fulfilled.
In Rust, for example, if you want to make your data structure human-readable, you implement the so-called “Display” trait. This trait is essentially a function that, called with the relevant data structure as its sole argument, returns some string representation of it.
That’s all. We don’t care whether the data structure has other ’traits’ or, if Rust were an OOP language, if it participated in some kind of hierarchy. From the point of view of that abstract type, it simply does not matter how the typed object relates to other objects.
In particular, the tagging model frees us from the necessity to stuff all kind of shared behavior in the “base” class at the bottom of the class hierarchy. Some animals make noises, and some don’t, period. There’s no need to consider whether making sounds is an essential characteristic of being an animal. In this way, behavior can be standardized without having to build (and maintain!) a complex taxonomy.
Abstraction as Construction #
With some distance, we must conclude that this ’tagging’ model offers a much more realistic view of abstraction. Abstracting is literally a process of removing the irrelevant, focusing on the essential – but always with an eye on the respective purpose. In other words: Abstraction is itself a contextualized operation; it captures what is essential for this particular purpose.
Let’s take numbers, one of the most powerful tools of abstraction ever invented. At first glance, it sems to confirm the traditional model. When we count things, we practically ignore every other feature, property or quality that belongs to them; we reduce them to numbers.
This concept has proven so useful that we have invented quite some tools that transform non-countable things, like liquids, into countable entities. And what is a computer, if not a machine converting meanings into numbers?
Yet in contrast to the traditional model, assigning numbers does not express an hierarchy. When counting things, we do not expect the things counted to belong to the same type. Numerically, there is no problem with comparing apples and oranges.
On the contrary, it is precisely this independence from the “essence” of things that gives counting its power. Counting would not get off the ground if we first had to know to which “essence” an object of counting belongs.
Yet this is precisely the way the OOP model works. It assumes that to be countable, everything must be part of a huge, unified taxonomy – which is why there is always a “base class” from which every object is derived. (See, for example, the definition of the ‘Object’ class in Java.)
This strong reliance on hierarchy reflects a conservative ontology, with a stark belief in rational order. In contrast, the use of tagging-style abstract types reflects a more nominalistic and pragmatic way of thinking. In this latter view, abstracting is not a process of identifying common features. Rather, it is perceived as a specific way of handling things.
Numbers, for example, don’t express any insight into the nature of being, but allow us to construct quite some new perspectives on it. Successful abstraction offers a new standpoint, a way to relate to the issue at hand. Its operative value, however, remains bound to this problem; abstraction can become stale, useless and itself an obstacle.
A good abstraction is like a tool, it gives you the power to do something with it. But it does not absolve us from the fact that we approach the world with that tool and the perspective its offers. Other tools are possible.
EDITS
- 01/06/2025 – In a previous version of this post, I had written that Java “finally” introduced interfaces. But it seems they are as old as Java 2. See this history of the concept.
-
It might be also a characteristic trait of Non-Western thinking, but who am I to know? ↩︎
-
For the philosophically inclined, this way of thinking closely models classical Aristotelian metaphysics: Every species is marked by its specific difference (differentia specifica) to a common species, to which it belongs. If you want to know what something is, focus on the differences to its common genus. ↩︎
-
As the philosopher John Dewey points out, the real provocation of Darwinism is that it radically breaks with this hierarchical Essentialism. For biology, kinds are fluid, adaptive and contextualized; and most importantly, they always change, and thus defy the human attempts of categorization (See Dewey’s „The Influence of Darwinism on Philosophy“. In: Philosophy after Darwin: Classical and Contemporary Readings, Ed. Michael Ruse, 2021). ↩︎