QIAOPANG.COM · SELECTED ESSAYS (2013–2026) · 260 ESSAYSBrowse Chronologically (2013 → 2026) ↗
QiaoPang · Programmers LifeSelected Essays · Systems, Craft & Agentic Era
Timeline ↗

On Object-Oriented Programming

On Object-Oriented Programming

Recently I’ve written a few essays on functional programming, and some readers have discussed with me the relative merits of functional programming and object-oriented programming. Both are excellent ways of thinking about code, both set out to solve the problem of code reuse, and each has absorbed the other’s strengths — so there is really no need to declare a winner. Yet compared with functional programming, object-oriented programming spans far more languages and a much broader audience, so its bar to entry is lower — and the volume of garbage books it has produced (at home and abroad alike) is correspondingly large, all too often leading readers in the wrong direction.

I’ve interviewed many engineers who claim to be proficient in object-oriented programming (Python / Ruby / C++, say). A few casual questions are enough to reveal how they actually understand it:

  • What do you think is the most important idea in object-oriented programming?
  • If someone mentions “inheritance,” I ask them to write out an example of inheritance they’ve used at work.
  • If someone mentions “polymorphism,” I ask them to explain polymorphism and then write out an example they’ve used at work.
  • If someone mentions “code reuse,” I ask them to talk about how they understand code reuse, with an example from their work.

For the first question, most people answer inheritance; some add interfaces, polymorphism, and similar concepts; very few mention code reuse.

For the second question, nearly 80% write code in which objects inherit from objects — and more than half of those give as their “real-world” example the classic beginner-corrupting example straight out of a famously bad book: Duck extends Bird, Bird extends Animal.

For the third question, nearly everyone writes polymorphism through object inheritance, and half of them again fall back on the famous example from that famous bad book: birds can fly and birds can sing; ducks go quack-quack but cannot fly. You can assign a duck object to a bird and make it quack.

For the fourth question — well, if someone actually gets that far and can rattle off Iterable, Comparable without breaking a sweat, then, assuming they haven’t read the essays on Program Life (heh), their fundamentals and their thinking about programming are pretty much where they should be, and we can move on to topics beyond objects.

The most fundamental and most important element in both object-oriented and functional programming is code reuse — or, more strictly, code being reused. Why insist on the distinction between “code reuse” and “code being reused”? Reusing code isn’t a problem at all: unless you’re writing assembly, nine out of every ten lines you write are reusing someone else’s code. Even a statement as simple as printing to the console — didn’t you use the language’s own library functions? And even if the language folds it into core syntax, that language’s implementation must still call some libC function underneath to obtain the operating system’s support. So in this line of work, reusing other people’s code is what we’re best at; getting our own code reused by others is where we usually come up short.

Anyone who has written Python for more than six months knows about the so-called magic functions. If your class definition implements certain magic functions, the class acquires some magical powers, for example:

class A:
    def __init__(self, items):
        self.__data = [item for item in items]
    def __getitem__(self, key):
        return self.__data[key]
    def __add__(self, data):
        return self.__data + [item for item in data]
    def __iadd__(self, data):
        self.__data.extend([item for item in data])
        return self
    def __iter__(self):
        return iter(self.__data)

The class above inherits from no known class (implicit inheritance from object doesn’t count), yet it can easily be called by other code in a common way:

a = new A([1,2])
b = new A([3,4])
a[0] # the object supports indexing, returns 1
a + b # the object supports addition, returns [1, 2, 3, 4]
a += b # the object supports in-place add; afterwards a.__data is [1, 2, 3, 4]
[item for item in a] # the object supports iteration, returns [1, 2, 3, 4]

This is the power of code being reused. This style of programming is better described as interface-oriented programming than as object-oriented programming. The object here is merely a front; its reason for existing is largely to satisfy some interface. In interface-oriented programming, interface inheritance matters far more than class inheritance.

Why is interface-oriented programming so important? Because it is a form of inversion of control — by abstracting out a set of interfaces and operating on those interfaces, the control logic no longer depends on any concrete implementation; at the same time, concrete implementations needn’t care how the control logic uses them, because they will be called when needed. The logic that uses the object and the object itself are thereby fully decoupled, with the interface serving as the bridge between them. In this way, code achieves the greatest possible reuse.

Speaking of interface inheritance, one cannot avoid the Liskov substitution principle. Wikipedia describes it thus:

It states that, in a computer program, if S is a subtype of T, then objects of type T may be replaced with objects of type S.

This is what object-oriented programming usually calls substitutability. In plain terms: in a type system, if type S is a subtype of type T, then any object of type T may be replaced by an object of type S without affecting the correctness of the program. The Liskov substitution principle is an extremely important principle in object-oriented programming (and indeed applies to any way of thinking about programming), and it is the cornerstone that makes polymorphism possible. If we are building a system, we must take every care to satisfy this principle.

What is polymorphism? Wikipedia explains it this way:

polymorphism (from Greek πολύς, polys, “many, much” and μορφή, morphē, “form, shape”) is the provision of a single interface to entities of different types. A polymorphic type is one whose operations can also be applied to values of some other type, or types.

From this we can see that polymorphism means we can apply a single operation (interface) to multiple data types. Polymorphism is by no means a concept exclusive to object-oriented programming; in functional programming it is everywhere. As we know, in Python a data structure can be passed to map only if it implements __iter__; in Clojure, likewise, a data type that implements ISeq can use map; and in Elixir, Enum.map requires the Enumerable protocol to be implemented. Clearly, polymorphism is not the exclusive patent of object orientation.

That jokey “birds can fly and birds can sing, but ducks go quack-quack and cannot fly” example we mocked above actually violates the Liskov substitution principle. It deprives your code of the benefits of polymorphism. Suppose you build a system around the base class “Bird,” and a later developer of the system makes “Duck” inherit from “Bird.” When your code performs the “fly” operation, since ducks cannot fly, the system may crash (or throw an exception). At that point you have no choice but to go back and modify the system built around the base class, handling every possible exception when “fly” runs (which is sometimes simply infeasible). This violates the Open-Closed Principle: for the sake of one clumsy implementation at the top of the hierarchy, you are forced to modify the code beneath it. The more situations like this a system has, the more bugs it has.

To conform to the Liskov substitution principle, a subclass must strictly inherit and implement its parent’s interfaces, and this raises a problem: in practice, the notion of a “class” is often treated as a taxonomy — that is, as is-a. is-a is a trap in object orientation, because when we judge whether something is-a something else, we often rely on the everyday logic of the real world, which is highly unreliable and produces plenty of clumsy designs — for instance, a parent type “Bird” that can fly with a subtype “Duck” that cannot, a design that violates the Liskov substitution principle while conforming to everyday logic. Using “Bird” and “Duck” to explain object-oriented inheritance may be intuitive and vivid, but it drops beginners into a mire from which they cannot extricate themselves.

In his famous Functional Programming Patterns, Scott Wlaschin observes: types are not classes. Equating “class” with “type” is another trap beginners commonly fall into. In functional programming, a type is really an interface — a bridge between data and the behaviors that data can produce:

On Object-Oriented Programming

And a “class” is one way of implementing a “type.” In this sense, “flyable” is a type; “Bird” implements flyable, whereas “Duck” cannot implement flyable, and therefore “Duck” is not a subtype of “Bird.” Once we understand this, we will no longer foolishly file “Duck” under “Bird” on the basis of everyday experience.

And once we understand this, we can also grasp why Java’s designers took such pains to use extends for class inheritance and implements for interface inheritance, and why, when a method is overridden, the return type is allowed to be covariant.

MORE IN Languages & Craft

Further Reading in This Column

View all essays in “Languages & Craft” →

Time to Meet jj: Git's Next Evolutionary Step

Git has ruled version control for twenty years, but its staging area, brittle rebases, and blocking merge conflicts are becoming liabilities in the AI era. jj rebuilds the mental model from the ground up — while staying fully Git-compatible. Here's why it deserves your attention.

New Hope (2025–2026)5,588 chars · 12 min read