There's some discussion on the internet these days that Object-Oriented Programming is a terrible idea. I agree, it is, but why is it a bad idea? What if I told you I think objects are a good idea, perhaps fundamental even?
One of the things that annoys me to no end in online discourse are people talking past each other. This usually happens because the topic is ill-defined, and Object-Oriented Programming (OOP) is ill-defined. There is no rigorous agreed-upon definition of what OOP is, which makes any sort of serious discussion about it difficult.
Is inheritance critical to OOP? Some years ago most people would tell you that yes, OOP is all about modeling inheritance (e.g. my university courses), whereas these days most people will tell you to prefer composition over inheritance.
I had the pleasure to discuss the subject with some wonderful folk from the Handmade community a while back. You can find the discussion here.
In this post I’m hoping to crystalize my thoughts on OOP. Rather than explore all that OOP is or could be, I'll propose my own minimal definition of what I think it is, and hopefully you'll agree it is a minimal but sufficient subset for discussion. First, some terminology:
An interface, as far as I'm concerned, is a set of operations an object is expected to implement. This can be anything from a Java interface, to a particular usage of Rust traits, to the implicit set of operations some Smalltalk object expects of another.
An object, as far as I'm concerned, is a pair of some encapsulated data, unknown from the point-of-view of the object's consumer, and an implementation of an interface. Java objects with private fields, Smalltalk objects, Rust dyn traits, and a C struct containing
void* data
plus a pile of function pointers are all objects.Object-Oriented Programming (OOP) is then, as far as I'm concerned, a programming paradigm where code is structured around objects (as defined above), in contrast to functional or procedural programming where the data and the functions that operate on that data are kept separate.
If you disagree with the above, feel free to comment below 😉.
Why would you want to structure (part of) your code as objects? You need them whenever you have an open set of data representations but a fixed set of operations on that data. A clear example is a plugin system, each plugin stores its own data unknown to the host, but it must implement a fixed interface expected by the host. The set of plugins is infinite, but the operations they must implement are finite.
This is in contrast to sum/union/variant-types, which have a finite set of possible representations (the variants) but an open set of operations (functions/procedures) that pattern match and operate on those variants.
The Expression Problem
This dichotomy, infinite-cases/finite-operations vs finite-cases/infinite-operations is known as the expression problem. The visitor pattern is a way to emulate the finite-cases/infinite-operations side in OOP languages. The expression problem is always there, and one way or another you need to deal with it.
Speaking of which, the expression problem is not something to solve IMO. If you don't have either a fixed set of variants, or a fixed set of operations, what the heck are you doing? Any function you write cannot possibly be written in a way that is future proof to new variants or operations. Consider for example a plugin system. You add a pair of new operations to it. Old plugins will still target the previous set. What have you accomplished that a plugin_v2 interface wouldn't have? Any potential "solution" to the expression problem is ultimately an overcomplicated way to accomplish nothing of value.
The expression problem is why I say objects are fundamental, whichever way you implement them. Just as object-oriented programming can emulate the other half of the expression problem with visitors, you can emulate “objects” in any number of ways. As far as I'm concerned, the following are objects:
def new_rect(x, y, w, h):
def implementation(cmd, data):
nonlocal x, y, w, h
if cmd == "move":
x += data["x"]
y += data["y"]
elif cmd == "scale":
w *= data["factor"]
h *= data["factor"]
elif cmd == "print":
print(f"square({x}, {y}, {w}, {h})")
else:
raise Exception("unknown message")
return implementation
def new_circle(x, y, r):
def implementation(cmd, data):
nonlocal x, y, r
if cmd == "move":
x += data["x"]
y += data["y"]
elif cmd == "scale":
r *= data["factor"]
elif cmd == "print":
print(f"circle({x}, {y}, {r})")
else:
raise Exception("unknown message")
return implementation
def play_with_shape(shape):
shape("move", {"x": 2, "y": 3})
shape("scale", {"factor": 2})
shape("print", {})
square = new_square(0.0, 0.0, 20.0, 10.0)
circle = new_circle(5.0, 5.0, 5.0)
play_with_shape(square)
play_with_shape(circle)
Perhaps this doesn't fit your view of what an “object” is? What if instead of a conditional, the dispatch was handled by a hash table of strings to functions? That's basically how most dynamic language runtimes work. The implementation is different but the purpose is the same.
With me so far? Objects (as a pattern) show up all time, they're a solution to a particular kind of problem, no matter the language you are using and whether it has classes or not. Language with OOP features like classes or whatever just make working with objects easier, just as languages with pattern matching make working with sum-types easier.
The venerable master Qc Na was walking with his student, Anton. Hoping to prompt the master into a discussion, Anton said "Master, I have heard that objects are a very good thing - is this true?" Qc Na looked pityingly at his student and replied, "Foolish pupil - objects are merely a poor man's closures."
Chastised, Anton took his leave from his master and returned to his cell, intent on studying closures. He carefully read the entire "Lambda: The Ultimate..." series of papers and its cousins, and implemented a small Scheme interpreter with a closure-based object system. He learned much, and looked forward to informing his master of his progress.
On his next walk with Qc Na, Anton attempted to impress his master by saying "Master, I have diligently studied the matter, and now understand that objects are truly a poor man's closures." Qc Na responded by hitting Anton with his stick, saying "When will you learn? Closures are a poor man's object." At that moment, Anton became enlightened.
What about OOP?
Ok, so that's what I consider objects to be. Pretty useful IMO, but what about Object-Oriented Programming? What does that actually entail? Well, it's structuring your entire program as one half of the expression problem, the infinite-cases/fixed-operations side. Sounds like a weird thing to do, no? Like coding with one hand behind your back. You can, sure, but why? What do you gain from it?
What does OOP actually give you? Really think about for a moment. Why was it considered to be the future of programming in the 90s? Plenty of reasons were given I'm sure, like how it's better at modeling the real world (it isn't) but I think the main reason was that it (supposedly) made your program code more modular and reusable.
Remember how the original example I gave for the importance of objects were plugin systems? OOP is basically making your entire program a giant plugin system, where you can replace individual parts without breaking the system as a whole. That's certainly true in Smalltalk, where you even do it while the system is *live*. If you have not experienced Smalltalk, I strongly suggest you do so, as it really shows what OOP is all about (or at least can be).
But there's Smalltalk, and then there's everything else. It's much more difficult to replace objects surgically like that in other OOP languages. You have to change every explicit instantiation of the object for a different one, or overcomplicated your code with Factories and Dependency Injection (and if you need to change the factory... welcome to AbstractFactory hell). Is all that complexity worth it to make your code (supposedly) more modular and reusable? HELL NO.
In the time it took you to set up that abstract nightmare nonsense you could have done a find-and-replace. That abstract nightmare nonsense also makes your code a nightmare to understand and debug, so congratulations, you played yourself.
Where am I going with this? I have no idea! I'm in full on ranting mode. OOP is stupid unless you have a Smalltalk-like live system. Use objects when they are useful, certainly, but there’s no reason to structure your whole program around them.
What about Inheritance? What about Modeling?
Ok, so I ignored inheritance almost entirely on the discussion above but it really deserves its own section, as does modeling (e.g. UML), as they are intimately tied to the object-oriented horrors of the late 90s and early 2000s.
Unfortunately this post is getting long, so I'll leave that for another time. TL;DR:
Trying to model a partial taxonomy of the world as a tree-shaped inheritance hierarchy and imposing that structure onto your codebase is one of the stupidest ideas we've ever had as a field and I'm glad we're finally moving away from it. The object-relational impedance mismatch is weakness of the object side, not of the relational.
Pointlessly structuring your code around real-world concepts isn’t a “sin” exclusive to the OOP paradigm, but it is certainly the worst offender. I might do a post on the subject in the future, but for now I can link you to this excellent article by Ryan Fleury: Emergence and Composition.
Anyway, I hope you can get something of value from this article, I wanted it to be more of an educational thing but somehow it turned into a rant. Do let me know in the comments how much of a dummy dumb dumb I am.