(2022-10-23) ugh, I think maybe I changed my mind; see “NOTE TO SELF”
What’s So Bad About Inheritance?
Object-oriented programming considered harmful”
In recent(ish?) years, the software community seems to have converged on the opinion that “object-oriented programming is bad”1. This has rubbed off on me and I have inherited2 the habit of generally avoiding classes; unfortunately, I did so without really understanding why “OO” is bad. Here I am to collect my thoughts and discuss what parts of OO actually are bad (and what aren’t), and why.
Specifically people are speaking about OO in the tradition of Java (as opposed to e.g. Smalltalk).
haha
What I mean by “object-oriented”
The termobject-oriented” has more than one meaning, so it’s worth pinning down what sense of OO is the one people have come to dislike. Namely, it’s programming characterized by the following:
Most values carry a reference to another value, which we’ll call its “prototype”
Prototypes can themselves carry references to prototypes (the “prototype chain”)
When resolving an attribute/method on a value, we follow the prototype chain. To find o.x, we first look to see if o has x; if not, we look at its prototype, and then it’s prototype’s prototype, and so on. (this is inheritance)
Most value share their prototypes with a large number of other objects3
That is, prototypes act to “classify”—or, if you will, “type”—values.
Much of the time a value with a prototype is called an “object”, and its prototype chain is said to be made up of its “class” and its class’s “superclasses”.4
I’ve specifically avoided talking about “objects” or “classes” in this definition. That is because the style of OO I am trying to pin-down does not at all rely on language features but is really a design pattern which can (in theory) be performed in any language.
The problem is overrides
The fundamental issue that I understand with this-style OO is specifically inheritance with overrides. Let me give an example5 and we can discuss it abstractly afterwards.
Credit where credit’s due. Both my understanding of why “OO is bad” and this example stem from an article I read years ago which slowly fermented in my mind. Unfortunately, despite repeated attempts, I have been unable to find this article. If I recall correctly, it spoke about the “three pillars” of object-oriented design (inheritance, polymorphism, and abstraction?) and how OO fails to adhere to all of them.
Say we are working in a context providing a linked-list abstraction adhering to the following interface class LinkedList { static new() // construct an empty list add(x) // add a single item addAll(xs) // add all items from another list length() // compute length iter() // implement some iterator protocol } Because LinkedList is a linked list, this means that its length() method runs in O(n) %% general %% % shorthands \newcommand{\cl}[1]{ \mathcal{#1} } \newcommand{\sc}[1]{ \mathscr{#1} } \newcommand{\bb}[1]{ \mathbb{#1} } \newcommand{\fk}[1]{ \mathfrak{#1} } \renewcommand{\bf}[1]{ \mathbf{#1} } \renewcommand{\sf}[1]{ \mathsf{#1} } \renewcommand{\rm}[1]{ \mathrm{#1} } \newcommand{\floor}[1]{ { \lfloor {#1} \rfloor } } \newcommand{\ceil}[1]{ { \lceil {#1} \rceil } } \newcommand{\ol}[1]{ \overline{#1} } \newcommand{\t}[1]{ \text{#1} } \newcommand{\norm}[1]{ { \lvert {#1} \rvert } } % norm/magnitude (REMOVE) \newcommand{\mag}[1]{ { \left\lvert {#1} \right\rvert } } % magnitude \newcommand{\smag}[1]{ { \lvert {#1} \rvert } } % short mag \newcommand{\card}{ \t{cd} } % cardinality \newcommand{\dcup}{ \sqcup } % disjoint untion \newcommand{\tup}[1]{ \langle {#1} \rangle } % tuples \newcommand{\tl}{ \tilde } \newcommand{\wt}{ \widetilde } \newcommand{\To}{ \Rightarrow } % draw a box outlining some math \newcommand{\box}[1]{ \fbox{$ #1 $} } % f \onall X = { f(x) : x ∈ X } \newcommand{\onall}[1]{ { \llbracket {#1} \rrbracket } } % shorthands: various brackets \newcommand{\tpar}[1]{ \left( {#1} \right) } % "tall parens" \newcommand{\tbrak}[1]{ \left[ {#1} \right] } % "tall brackets" \newcommand{\tbrac}[1]{ \left\{ {#1} \right\} } % "tall braces" % reverse \mapsto (FIXME: make better) %\newcommand{\mapsfrom}{ \mathop{\leftarrow\!\mid} } \newcommand{\mapsfrom}{ \mathrel{↤} } % reverse-order composition \newcommand{\then}{ \operatorname{\ ;\ } } % Like f' represents "f after modification", \pre{f} % represents "f before modification" % TODO: remove this? \newcommand{\pre}[1]{{ \small `{#1} }} % hook arrows \newcommand{\injects}{ \hookrightarrow } \newcommand{\embeds}{ \hookrightarrow } \newcommand{\surjects}{ \twoheadrightarrow } \newcommand{\projects}{ \twoheadrightarrow } \newcommand{\id}{ \,\mathrm d } % integration d % derivatives: use {\ddn n x y} for (dy/dx) \newcommand{\ddn}[3]{ \frac{ {\mathrm d}^{#1} {#2} }{ {\mathrm d} {#3}^{#1} } } % nth derivative \newcommand{\dd}{ \ddn{} } % first derivative \newcommand{\d}{ \dd{} } % first derivative (no numerator) \newcommand{\dn}[1]{ \ddn{#1}{} } % nth derivative (no numerator) % derivatives: use {\D n x y} for (∂_x y) \newcommand{\Dn}[2]{ \partial^{#1}_{#2} } \newcommand{\D}{ \Dn{} } % no power \newcommand{\ig}[2]{ \int {#2} \, \mathrm d {#1} } % first integral %% category theory %% % category names \newcommand{\cat}[1]{{ \sf{#1} }} % yoneda embedding \newcommand{\yo}{よ} % extra long right-arrows \newcommand{\X}{-\!\!\!-\!\!\!} \newcommand{\xlongrightarrow}{ \mathop{ \, \X\longrightarrow \, } } \newcommand{\xxlongrightarrow}{ \mathop{ \, \X\X\longrightarrow \, } } \newcommand{\xxxlongrightarrow}{ \mathop{ \, \X\X\X\longrightarrow \, } } \newcommand{\takenby}[1]{ \overset{#1}{\rightarrow} } \newcommand{\longtakenby}[1]{ \overset{#1}{\longrightarrow} } \newcommand{\xlongtakenby}[1]{ \overset{#1}{\xlongrightarrow} } \newcommand{\xxlongtakenby}[1]{ \overset{#1}{\xxlongrightarrow} } \newcommand{\xxxlongtakenby}[1]{ \overset{#1}{\xxxlongrightarrow} } % represents an anonymous parameter % eg. $f(\apar)$ usually denotes the function $x \mapsto f(x)$ % TODO: remove this? \newcommand{\apar}{ {-} } %% computability %% % turing machines \newcommand{\halts}{ {\downarrow} } \newcommand{\loops}{ {\uparrow} } O(n) time. Sometimes this is acceptable; sometimes it isn’t. Let’s say that we want a faster length() operation. Well, we know how to solve this—subclass LinkedList and override its methods to keep track of how long the list is at all times. class OurList extends LinkedList { static new() { super.new(); this.length = 0; } add(x) { super.add(x); this.length++; } addAll(xs) { super.addAll(xs); this.length += xs.length(); } length() { return this.length; } } 6 Kerblam! A few lines of code and now we have O(1) %% general %% % shorthands \newcommand{\cl}[1]{ \mathcal{#1} } \newcommand{\sc}[1]{ \mathscr{#1} } \newcommand{\bb}[1]{ \mathbb{#1} } \newcommand{\fk}[1]{ \mathfrak{#1} } \renewcommand{\bf}[1]{ \mathbf{#1} } \renewcommand{\sf}[1]{ \mathsf{#1} } \renewcommand{\rm}[1]{ \mathrm{#1} } \newcommand{\floor}[1]{ { \lfloor {#1} \rfloor } } \newcommand{\ceil}[1]{ { \lceil {#1} \rceil } } \newcommand{\ol}[1]{ \overline{#1} } \newcommand{\t}[1]{ \text{#1} } \newcommand{\norm}[1]{ { \lvert {#1} \rvert } } % norm/magnitude (REMOVE) \newcommand{\mag}[1]{ { \left\lvert {#1} \right\rvert } } % magnitude \newcommand{\smag}[1]{ { \lvert {#1} \rvert } } % short mag \newcommand{\card}{ \t{cd} } % cardinality \newcommand{\dcup}{ \sqcup } % disjoint untion \newcommand{\tup}[1]{ \langle {#1} \rangle } % tuples \newcommand{\tl}{ \tilde } \newcommand{\wt}{ \widetilde } \newcommand{\To}{ \Rightarrow } % draw a box outlining some math \newcommand{\box}[1]{ \fbox{$ #1 $} } % f \onall X = { f(x) : x ∈ X } \newcommand{\onall}[1]{ { \llbracket {#1} \rrbracket } } % shorthands: various brackets \newcommand{\tpar}[1]{ \left( {#1} \right) } % "tall parens" \newcommand{\tbrak}[1]{ \left[ {#1} \right] } % "tall brackets" \newcommand{\tbrac}[1]{ \left\{ {#1} \right\} } % "tall braces" % reverse \mapsto (FIXME: make better) %\newcommand{\mapsfrom}{ \mathop{\leftarrow\!\mid} } \newcommand{\mapsfrom}{ \mathrel{↤} } % reverse-order composition \newcommand{\then}{ \operatorname{\ ;\ } } % Like f' represents "f after modification", \pre{f} % represents "f before modification" % TODO: remove this? \newcommand{\pre}[1]{{ \small `{#1} }} % hook arrows \newcommand{\injects}{ \hookrightarrow } \newcommand{\embeds}{ \hookrightarrow } \newcommand{\surjects}{ \twoheadrightarrow } \newcommand{\projects}{ \twoheadrightarrow } \newcommand{\id}{ \,\mathrm d } % integration d % derivatives: use {\ddn n x y} for (dy/dx) \newcommand{\ddn}[3]{ \frac{ {\mathrm d}^{#1} {#2} }{ {\mathrm d} {#3}^{#1} } } % nth derivative \newcommand{\dd}{ \ddn{} } % first derivative \newcommand{\d}{ \dd{} } % first derivative (no numerator) \newcommand{\dn}[1]{ \ddn{#1}{} } % nth derivative (no numerator) % derivatives: use {\D n x y} for (∂_x y) \newcommand{\Dn}[2]{ \partial^{#1}_{#2} } \newcommand{\D}{ \Dn{} } % no power \newcommand{\ig}[2]{ \int {#2} \, \mathrm d {#1} } % first integral %% category theory %% % category names \newcommand{\cat}[1]{{ \sf{#1} }} % yoneda embedding \newcommand{\yo}{よ} % extra long right-arrows \newcommand{\X}{-\!\!\!-\!\!\!} \newcommand{\xlongrightarrow}{ \mathop{ \, \X\longrightarrow \, } } \newcommand{\xxlongrightarrow}{ \mathop{ \, \X\X\longrightarrow \, } } \newcommand{\xxxlongrightarrow}{ \mathop{ \, \X\X\X\longrightarrow \, } } \newcommand{\takenby}[1]{ \overset{#1}{\rightarrow} } \newcommand{\longtakenby}[1]{ \overset{#1}{\longrightarrow} } \newcommand{\xlongtakenby}[1]{ \overset{#1}{\xlongrightarrow} } \newcommand{\xxlongtakenby}[1]{ \overset{#1}{\xxlongrightarrow} } \newcommand{\xxxlongtakenby}[1]{ \overset{#1}{\xxxlongrightarrow} } % represents an anonymous parameter % eg. $f(\apar)$ usually denotes the function $x \mapsto f(x)$ % TODO: remove this? \newcommand{\apar}{ {-} } %% computability %% % turing machines \newcommand{\halts}{ {\downarrow} } \newcommand{\loops}{ {\uparrow} } O(1) runtime for the length() method. Plus, because our class extends LinkedList, it can be used as if it were a LinkedList. In other words, most of the codebase doesn’t even have to know that OurList exists for us to reap the performance benefits!
I hope you can forgive my strange code formatting. The intent behind it is to increase the vertical density of the writing, so that a reader can more efficiently skim and traverse it.
Great, except that this code is fundamentally wrong. The OurList implementation is at worst broken and at best can break at any moment. Let’s say that LinkedList implements addAll like this: class LinkedList { add(x) { ... } addAll(xs) { for (x in xs) this.add(x); } } Now what happens when we call OurList.new().addAll(xs)? It first calls super.addAll(xs), which dispatches LinkedList.addAll. This implemention loops over each x in xs and calls this.add(x) on each. this.add dispatches to OurList.add, meaning that each x gets added to the list and causes this.length to get incremented. Then, once LinkedList.addAll completes, we return to OurList.addAll and again increment this.length. We’ve double-counted! Ooookayyyy... So remove the this.length += xs.length() line, right? Increment length only on add() but not addAll(). Problem solved? Still no. Another reasonable implementation for LinkedList looks like this: class LinkedList { addAll(xs) { ... } add(x) { this.addAll([x]); } // [x] is the singleton array } Now we have the same problem for OurList.add instead of addAll. Calling OurList.new().add(x) will dispatch through LinkedList.add to this.addAll which is OurList.addAll, which will increment length; then, OurList.add will again increment length. Because LinkedList makes no guarantees about how it implements add and addAll, it’s unsafe to add length increments in either case.
What went wrong?
We wanted to have a class OurList extends LinkedList which kept a length counter. We found that the obvious way to do this is to override add and/or addAll to increment an internal length counter. However, both options are unsafe because they depending on implementation details of LinkedList. And that’s the essential issue: overrides encourage poor abstraction boundaries. On the one hand, a subclass ought to not know or care about how its superclass is implemented. This is the point of a class, after all—it’s supposed to be a black-box abstraction. However, we’ve seen that practically this just isn’t the case. If we want—for example—OurList to work properly, it has to know about how LinkedList implements add and addAll. More abstractly, we can phrase the issue like this:
Say we have class Lo extends Hi where Lo overrides the m method on Hi
eg, OurList extends LinkedList overriding addAll
The API for Lo wants to make some guarantees about what m does
eg, guaranteeing addAll increments length properly
In order to do so, Lo may need to make some assumptions about the implementation of Hi
eg, that LinkedList.addAll does not call this.add
Such an assumption is usually a violation of the class abstraction boundary7
A little disambiguation here. LinkedList may very well guarantee that "LinkedList.addAll does not call this.add". If this is the case, one can implement OurList.add in a way that does not make invalid assumptions. (NOTE TO SELF—uhhhh yeah doesn’t that actually make this all fine?) However, this guarantee—that "LinkedList.addAll does not call this.add"—views classes as boxes of code, rather than as abstract objects, which is arguably problematic in its own right.

(section — what’s NOT the issue here — interface inheritance. classes are bad only b/c closed namespaces. instanceof is usually bad.) (section — so just don’t use inheritance? — then classes are pointless, all a class is is an implicit namespace and dynamic dispatch is moot)