Fifty Shades of OOP
76 points by LesleyLai
76 points by LesleyLai
I greatly prefer this to some of the other, more rabid takes on OOP which I've read recently. I like that you've covered some of the tradeoffs more thoroughly, and from the perspective of someone who clearly has deep familiarity with the concepts.
That said, I'm disappointed every time I read another discussion about the definition of and tradeoffs involved with OOP which does not mention the perspective of William Cook, who in my mind was one of the greatest authorities on the topic. In On Understanding Data Abstraction, Revisited he makes the case that encapsulation is the single most important aspect of object orientation, and that things like classes, inheritance, and mutability are orthogonal. He follows this up with the blog post A Proposal for Simplified, Modern Definitions of "Object" and "Object Oriented", where he makes this more explicit with a deeper discussion. A later paper by a different author, The power of interoperability: Why Objects are Inevitable makes the case that Cook's definition matches the implementation and explains the success of most large-scale software frameworks, regardless of language. The author's thesis is that whether in a primarily OO language like Java or in a very non-OO language like C, building large scale frameworks almost universally requires either using the language's built-in OOP features, or reinventing objects as a higher-level pattern.
I think a great deal of strife would be saved if more people were familiar with this work. I get indigestion every time I see a React developer claim the superiority of functional programming over OOP, as though the two are incompatible, and as though the framework that they've built their career on isn't an example of both.
Had never heard about Cook, thanks for sharing! It's true that encapsulation is quite unique to objects, although often not enforced (eg. Python, where objects are glorified structs). I'd say myself that the interaction/message-based communication that Kay talks about is what most people don't see about objects, and what sets that model apart (even from "traditional" OOP, like C++ vs Objective-C). Specifically, it predates the actor model and helps think about systems in terms of interactions (behaviour) as opposed to structure (data, classic approach). With today's emphasis on concurrency and multi-agent systems there's a lot of good stuff to revisit from these lesser known properties of the OOP model
It's true that encapsulation is quite unique to objects
Uhhhh hang on, what?
Objects are one of many ways to accomplish encapsulation.
Objects are one of many ways to accomplish encapsulation.
what are these other ways and in which way aren't they objects with a different name?
Closures. Abstract data types. Modules.
None of them are OOPy since they don’t involve methods or classes or inheritance.
Closures
This is an old quote: "Closures are a poor man's objects, and objects are a poor man's closures."
Abstract data types. Modules.
It's hard to get more OO than ADTs and modules (assuming the original Algol/Simula meaning of the term).
"Closures are a poor man's objects, and objects are a poor man's closures."
That’s because they each lack key features of the other that the programmer has to put in extra work to emulate.
Objects don’t capture from lexical scope and are syntactically much heavier.
Closures make it hard to support multiple methods on the same data, and inheritance is even more difficult.
ADTs and modules date from the 1970s so they postdate Algol and Simula. They are common in non-OOP languages, so they aren’t very OOPy; I already listed several other feature that are far more OOPy.
That’s because they each lack key features of the other that the programmer has to put in extra work to emulate.
You seem to be approaching this topic with some negativity, and I don't fully understand why. Closures are just a tool. Objects are just a tool. People who have extensively used both (and who were using these long before us) realized that they were able to use these tools in largely interchangeable manners. The quote isn't somehow a complaint about OO programming, or a complaint about FP programming, or whatever. These are tools, and most engineers are just looking for good ways to solve problems, and these tools are useful tools.
Objects don’t capture from lexical scope and are syntactically much heavier.
Objects can capture from lexical scope. Languages differ in what capabilities they implement.
"syntactically much heavier" is a criticism of specific OO languages, which is not what was being discussed here.
I don't know where this negativity is originating from, but it seems to blind you.
ADTs and modules date from the 1970s so they postdate Algol and Simula. They are common in non-OOP languages, so they aren’t very OOPy
Modules were originally introduced as an extension to Algol 68 (IIRC), sometime shortly after it was released. They weren't specific to OOP, and were introduced originally in some imperative flavor of Algol that I've never used. I'm not suggesting that modules === OOP. This isn't religion for me. These are just tools. There are a lot of OO languages that don't support modules, and a lot of languages that support modules that aren't OO. What I said was that "It's hard to get more OO than ADTs and modules (assuming the original Algol/Simula meaning of the term)." I stand by that statement.
I don’t mean to be negative, I’m just trying to highlight the differences between language features. Obviously there’s a lot of overlap, and with a little effort a programmer can make (for example) closures do the job of objects or vice versa. But the similarities aren’t particularly informative if the question is what is distinctive about OOP.
And in fact it isn’t hard to get more OO than ADTs and modules: classes and inheritance and methods are more OO. If you ask a programmer what programming language features are OO they’ll list lots of things before getting to ADTs and modules.
More generally, I’m approaching this discussion as a question of what programming languages and language features are OO, not as a question of what programming styles or architectures are OO. Just because a programmer can build an OOPy system in a non-OO language, doesn’t mean that the language and features they used have suddenly become OO. What does make a feature OO is whether it is common in OO languages and rare in non-OO languages.
My litmus for whether a language is OO or not is, would it have been called OO in the 1990s: does it have inheritance, classes (or maybe prototypes), dynamic dispatch, methods (or maybe generic functions). In the last couple of decades the distinction has been muddied in ways that I don’t find helpful, whereas the 1990s definition is still useful for distinguishing the post-OO languages from the classically-OO languages. In particular the post-OO languages have typically ditched inheritance in favour of composing interfaces, and they have a greater emphasis on value types so that fewer things have to be references to mutable state.
Thanks for taking the time to post this. I really like your comment, and find myself in agreement with much of it, and respecting the parts that I don't agree with as reasonable and easily understandable (given my own personal experiences).
And I do agree that inheritance, more than any other touchstone, is tightly and irrevocably intertwined with the history of "OO". It seems to be the magnet (a lodestone) that attracted people to "OO". And having gone through the transition from pre-OO to OO (myself in the early 90s), I can attest to the strange and magical attraction that inheritance had on me, and the silliness that I now view that attraction in retrospect. Don't get me wrong: Inheritance can be quite useful and powerful, but it's just one tool in the toolbox, and the fascination with that tool far outweighs its ideal level of relevance. I think one of the reasons that inheritance was so attractive was the amount of "cut and paste" we found that it could eliminate in common cases; before inheritance, the cut-and-past nonsense really was insane. My boss in an early job once said (unironically), "You can replace any if with just another layer of indirection." Years later, we still joke about that line 🤣
Just because a programmer can build an OOPy system in a non-OO language, doesn’t mean that the language and features they used have suddenly become OO. What does make a feature OO is whether it is common in OO languages and rare in non-OO languages.
Over time, for me, OO has become a means of code organization more than anything. Even now, whenever I code in C, I am often using an "OO methodology" as the basis for organizing data structures and related code, as if the resuling C code areas were akin to classes. But C is definitely not an OO language.
I'd argue that an OO language is one that naturally "enforces" these approaches. A class-based OOPL for example will largely force you to group state together using classes, and to hang related functionality together on that class. This is what I mean by OO being largely "organization".
before inheritance, the cut-and-past nonsense really was insane
I'm really curious about this. I've heard it elsewhere too, inheritance as a means to re-use code that would otherwise be duplicated, but I've never understood it. Even when I was programming in Java long ago, I don't think I ever made one concrete type inherit from another, and I don't recall ever having issues with duplicated code. This across Python, OCaml, Java, Haskell, Pyret, Rust, C++.
Are there features that these languages have that lets you avoid the problem? Is it the ability to have a "default implementation" of a method, like Rust allows directly and like you can achieve in Java by "inheriting" from an abstract base class? (I wouldn't really call that inheritance but if that's all that people meant when saying that inheritance is necessary to avoid duplicating code that I guess that explains it.) Did developers back in the day not realize that you could like... do composition by having one object contain another? Was it an artifact of the fact that Java bundles visibility together with class definitions, such that abstraction boundaries had to be class boundaries, making composition difficult to use while making an abstraction?
To be honest, I don't have an entirely rational explanation, because going back to do the same tasks now in the same languages (e.g. GUI programming in C), I would fundamentally structure things differently, and not have the same insane level of cut-and-paste as I was used to seeing pre-OO. I can remember reading other people's 1000-line .c functions -- just to create and lay out a window (using the Win16 APIs). Each hwnd took 10-20 lines of code to create and configure, and everything (buttons, labels, text areas) were each their own hwnd. These are things that I'd expect to take 1-2 lines of code each in any modern framework, assuming you were even creating them with code instead of using a data-driven (more declarative) model, but back then you'd have page after page after page of code, creating these things by hand and wiring them together by hand. And with basically no error checking or recovery.
Part of the change, if I had to guess, is that the cost models changed. And part of that "cost model" wasn't just the number of CPU cycles that it took to call a function (because OO suddenly had lots more small functions, which was part of the early OO fetish, and even worse it often relied on indirect calls which were -- and still are -- far slower), but also the mental cost of keeping track of all those functions. By organizing them by type, the surface area went from 10,000 functions in a flat namespace API (e.g. all C APIs) to 100 types in an API, which was easier to categorize and memorize in one's head. Sure, each of those 100 types had 100 methods, but now you could find them easily (eventually via code completion in an IDE) because they were hierarchically organized. Want to do something with a button? The answer is on the Button class -- just one place to go. With inheritance, you might have to look at its super class, but that also meant that the code on the super class didn't have to get repeated on each control type.
Before classes, you'd scroll through 10,000 API functions looking for every single one that might take an hwnd to see if it was the magic one you were looking for. (Remember, this is long before Google existed, which in turn is long before Chet Jippity showed up.) Seriously, I probably read (scanned through) the entirety of the Windows API documentation no less than 500 times. Sometimes more than once in a day, because I'd get to the end without finding what I was looking for so I'd go back to the beginning and start scanning (or "find-nexting") again. It was so bad and convoluted and complex that Microsoft developers literally named one of their most common window controls "BozosLiveHere", and that control was used in pretty much every Windows application ever built. The amount of complexity you'd have to keep track of back then was insane.
Did developers back in the day not realize that you could like... do composition by having one object contain another? Was it an artifact of the fact that Java bundles visibility together with class definitions, such that abstraction boundaries had to be class boundaries, making composition difficult to use while making an abstraction?
I really do think it more came down to a shift in the cost model. For example, once you accepted that you were no longer responsible for tracking every single allocation in your application, things became a lot easier, so the advent of GC becoming popular (dating back to the 50s with Lisp I guess, but in the mainstream: Smalltalk, then Java) allowed designs that previously were impossible. That 1000 line function I described could now be 200 lines, and it would now actually be correct w.r.t. error recovery -- developers used to not even bother checking error returns because the code was already overwhelming and they had no easy way to structure error recovery. I'd say that between (i) error handling and (ii) resource management, that probably accounted for somewhere between 60-90% of code back then (even with poor error handling). Maybe even higher than that. But once you allowed yourself to rely on exceptions and GC instead, that probably dropped to under 10%.
The other aspect of the shift was the ease of dealing with a larger overall design because of its hierarchical organization. For example, you could now add 50 handy methods to your Button class precisely because they didn't pollute the global namespace. Before that, you'd simply "inline" that functionality everywhere it was needed, i.e. cut and paste. It's not that it was impossible to have that functionality in functions before OO -- it's that the "mental" cost of each function was super high because there was only one namespace (everything was global). So it's hard to overstate the importance of the organizational aspect of "OO". To be fair, this is the same reason why modules are such powerful tools! So you can probably now see what I meant in my earlier comment about modules being very OO-ish.
I think the prevalence and popularity of client-based GUI programming which coincided with OOP explains much of its hype and popularity. After all, that was programming at the time - writing desktop applications targeting Windows (and Mac, to a lesser degree).
I remember Visual Basic getting object-orientated at the backend code level (VB6?) but surely the "visual" part was objects of some sort before then.
Chet Jippity
Love this.
Over time, for me, OO has become a means of code organization more than anything.
Yeah, I think that’s also the basis of Alan Kay’s definition. For him Smalltalk was a means to express his ideas about system design and interactive computing; he was less interested in it as a language or notation. But of course the unwashed masses (*) of programmers (like humans in general) tend to focus on the surface appearance of something, instead of grappling with the underlying ideas. (eg, Wadler’s Law) So, at its peak, OOP was talked about more as a programming language thing than as a system design thing. As the hype dissipated the discussion of OOP became more nuanced, but it’s rare for anyone to be clear about whether they are talking about OOP as a programming language thing or as a system design thing. (Often aspects of system design are described in terms of programming language features.) Which leads to what I consider a category error of saying a non-OO language is OO because it has great support for certain kinds of OO system designs.
The uniquely distinguishing feature of OOP in the 1990s was inheritance, both in terms of programming languages and system design. Nowadays people seem to use OO to mean any kind of loosely-coupled system, which I think is a shame. There are lots of different styles for designing systems and it would be nice to have a richer vocabulary for talking about them. I guess we now have to use inheritance-oriented for what used to be called object-oriented.
(*) I checked whether that phrase has roughly the right amount of toung-in-cheek (derog.) and delightfully it turns out to have been coined by Bulwer-Lytton
I feel that the allure of inheritance is simply in how natural it feels: of course a Car is a Vehicle and a Truck is a Car, just like a display or keyboard is an I/O device, or like an Employee is a HumanUser. It all makes sense now!
But of course, nothing is ever quite so simplistic and introducing those hardened inheritance graphs just adds a new axis to manage into your problem: it very easily goes from essential to accidental complexity.
All of those are objects.
Closures are objects https://wiki.c2.com/?ClosuresAndObjectsAreEquivalent
Modules are objects in at least javascript and python. Ocaml folks have differing definitions for modules and objects but their definition is much more restrictive than the normal programming meaning of object ; I'd argue ocaml module are 100% objects in e.g. the js / python sense and what ocaml calls object is a subcategory which is runtime polymorphic object.
An instance of an ADT is absolutely an object in every non-theoretic programming language. Hell, in c++ "123" is an object (https://eel.is/c++draft/basic.memobj#intro.object) like everything in the language that can be created and isn't a reference
I thought we were discussing objects in the object-oriented sense, and what distinguishes them from related constructs in non-OOP languages. If everything gets to be an OOP object then OOP ceases to mean anything. The point here being that there are language features that provide encapsulation, and those features don’t have to be tangled up with specifically-OOP machinery such as inheritance.
I don't see how this follows from this comment chain which is discussing whether the term "object" encompasses "encapsulation" or not
Closures, header files (not saying this is a good way to do it but it is in widespread use), actors, microservices.
Of course if you're willing to stretch your definition of "objects with a different name" then maybe some of these apply, but that feels more like a No True Scotsman argument than a good-faith discussion.
Objects don’t capture from lexical scope and are syntactically much heavier.
Actors. Hm...:
Erlang might be the only object oriented language because the 3 tenets of object oriented programming are that it's based on message passing, that you have isolation between objects and have polymorphism." (Third point at http://www.infoq.com/interviews/johnson-armstrong-oop).
Microservices. Hmm...
In computer terms, Smalltalk is a recursion on the notion of computer itself. Instead of dividing "computer stuff" into things each less strong than the whole—like data structures, procedures, and functions which are the usual paraphernalia of programming languages—each Smalltalk object is a recursion on the entire possibilities of the computer. Thus its semantics are a bit like having thousands and thousands of computers all hooked together by a very fast network.
I mean like I said you can No True Scotsman your way out of this argument if you're really determined.
If your definition of encapsulation is "that's what OOP does" then obviously all cases of encapsulation will be classified as objects. But that's not what normal people mean by these terms.
This is not a "No True Scotsman".
Those are the people who created the term. So they might have some rough idea as to what it means.
Alan Kay went to OOPSLA saying that microservices were objects iirc.
I don't think header files themselves are what provide encapsulation - it's more the fact that you can put your implementation in a different place than your definition, but you loose some important properties that way.
Do index handles count? You have a (possibly discriminated) index which can be used to access multiple memory areas, all of which contain data of this index. This is not very object-like since the data is not stored together, and the handle is likewise not very object-like since it alone is useless. You need both the correct areas and the key to access data, yet this does enable encapsulation of the data.
One can of course argue that the encapsulation isn't performed by the handle or the areas, but by some encapsulation boundary that stops outsider access into the areas. And then one can argue that that encapsulation boundary is just an object in disguise, but I at least do feel arguing that eg. modules are just objects is quite disingenuous.
In a general way, yes of course, closures would be an easy example. But I meant it more as a concept/principle, or as one of the comments put it in the thread, an added constraint that makes systems more resilient by loosening (or clarifying) coupling. Like the comment on actors mentioned, it's about internalizing a state and interacting through messages (method calls). Poor phrasing on initial comment, should have been more specific.
true that encapsulation is quite unique to objects
I think that actors offer encapsulation, arguably stronger that in objects. Most paradigms, including OO, have some appropriate use cases.
CTM contains an exceptionally good discussion of this topic. I miss more PLT research on typed multiparadigm languages. AliceML was a joy, but it's kinda abandonware now.
Python can actually (almost completely) enforce encapsulation with fields that start with a double underscore. It is renamed in such a way that you can't type it from outside the class definition (although if you are really motivated you can use getaddr).
It is just almost never used.
Python can actually (almost completely) enforce encapsulation with fields that start with a double underscore. It is renamed in such a way that you can't type it from outside the class definition
Cool, I've never come across this "name mangling" in Python before:
>>> class A(object):
... __foo = 42
...
>>> a = A()
>>> a.__foo
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'A' object has no attribute '__foo'. Did you mean: '_A__foo'?
Interestingly, it doesn't seem to trigger when there are trailing underscores as well (which I attempted first, and was going to call BS!):
>>> class A(object):
... __foo__ = 42
...
>>> a = A()
>>> a.__foo__
42
When I want "proper encapsulation" in Python, I tend to use scope; but even those can be accessed with enough effort, e.g.
>>> f = (lambda foo: lambda bar: foo + bar)(42)
>>> f.__closure__[0].cell_contents
42
Yes, trailing double under is excluded. IIUC this is because this format has traditionally be used for methods with special meaning like __eq__ so breaking those would not be acceptable.
Interesting that the name is _A__foo. I thought it was something like __A__foo so that attempted accesses would be rewritten (as they also start with __). However even then it seems that outside of a class __foo isn't rewritten as your logs show. So even if it was __A__foo you would be able to access outside of a class definition but not inside (as it would be rewritten to __OtherClass__A__foo) which would be confusing.
So I guess the encapsulation isn't as good as I thought that it was. The name ends up being easily predictable and trivial to type manually.
he makes the case that encapsulation [...]
The most interesting critic I read[1] regarding encapsulation in OOP is that it's not really effective: it does not prevent against concurrent access by multiple threads. While that was not the intended use-case when encapsulation was conceived, I found the point is valid. Protecting against memory corruption seems more valuable than preventing external users with poking with the internals of an object, no?
OOP done like in Erlang/Elixir with almost-actors encapsulates and prevents against multiple thread access.
[1] in some blog posts (I don't remember the source). There was also once a GIANT blog post with ton of critics against OOP.
I think this is bringing in a few orthogonal issues. The conversation is more likely to be productive if you read the first paper I linked in my post, but briefly:
Concurrency control: this is unrelated. Mutable abstract data types can lack thread safety, and immutable objects can be thread safe. The goal of encapsulation is not to be a freestanding mechanism for preserving memory consistency in a multithreaded environment; this is a completely different class of problems.
“preventing external users with poking with the internals of an object”: This shows what I think is a common misconception around encapsulation, probably based on the way it’s usually introduced to beginners. The most beneficial point of encapsulation is not to try to somehow protect your code from your users; it’s to define abstractions which can be used polymorphically. I think of it in a similar vein to the idea that “constraints liberate, liberties constrain”. Accessing the internals of a component is a tradeoff: you gain the ability to “do more” with that component, and you lose the ability to swap out that component for other implementations of that component’s interface. Encapsulation allows extension via new implementations of existing interfaces. We take on a constraint (don’t poke at things that you shouldn’t) in order to also gain a liberty (we can now add new functionality in a modular way). It’s not a developer-versus-user thing; it’s a tradeoff that a single developer can benefit from within their own project.
I think this is spot on: encapsulation as an enforceable constraint to guarantee the boundary/surface of reuse.
in some blog posts (I don't remember the source). There was also once a GIANT blog post with ton of critics against OOP.
No shortage of those 🤣 ... you might be referring to one of these:
Casey Muratori's "The Big OOPs: Anatomy of a Thirty-five-year Mistake" - https://www.youtube.com/watch?v=wo84LFzx5nI
"Object-Oriented Programming is Bad" video - https://www.youtube.com/watch?v=QM1iUe6IofM
"Could encapsulation be ... bad?" blog - https://medium.com/lost-but-coding/could-encapsulation-be-bad-5f5912a11bde
The most interesting critic I read[1] regarding encapsulation in OOP is that it's not really effective: it does not prevent against concurrent access by multiple threads.
That's because most OO languages predate the existence of threads. Smalltalk was the 70s, C++ was the 80s, Java and Python (single-threaded) were both developed in '92. Java wasn't released until '95, and by that 1.0 it did include thread support, which I believe was a first for a new PL. Solaris may have been the first major OS with thread support (via light-weight processes). Windows added its first support for threads in 1993 (the original "NT" release), and Linux only got its first (and half-baked) support for threads in 1996. It was another decade (Intel "Conroe") before typical computers could support more than one physical thread.
Protecting against memory corruption seems more valuable than preventing external users with poking with the internals of an object, no? OOP done like in Erlang/Elixir with almost-actors encapsulates and prevents against multiple thread access.
Erlang processes are a great example of how multi-threaded systems can safely deal with state, but Erlang didn't really have issues anyway thanks to a general reliance on immutability. But Erlang processes weren't designed for multi-threading; Erlang (Beam) was built for non-preempting single-physical-thread hardware (e.g. routers and switches) and didn't even try to support multi-threading until 2006 😳
Ecstasy (which I work on) uses a similar concept, called a service, which represents a single physical thread von Neumann machine that executes non-preemptable fibers. This allows all code to be emitted as efficient single-threaded code, with concurrency and parallel execution easily achieved just by having more than one service with work to do (i.e. any runnable fibers). For example, in a web app, each request is dispatched to a separate service. Calls to a different service are implemented as messages (that produce a future), while calls within a service are just normal function calls.
I found the article and posted it here https://lobste.rs/s/bxggw5/case_against_oop_is_understated_not
Thanks for sharing! I am not aware of Cook. I will definitely read the articles you've shared.
Wow, comprehensive stuff!
I like that you tackle the difficulty in defining what OOP even is. 3 decades ago, I was taught that OOP is more-or-less defined by encapsulation, inheritance, and polymorphism. All tackled in the article, but I feel like these just don't give an intuitive definition of OOP. If I had to try, I would say that OOP comes down to the concept of the "method." That feels to me like the common-denominator of the variety of programming techniques we see in stuff labeled OOP.
It's interesting to think about classes vs. prototype-based approaches. To me, if there's a way to get from an object to what constructed it (like there is in JavaScript) then the two seem to be mostly the same thing but expressed through different mental models. I feel like in a class-based approach the method-dictionary is sort of an implementation-detail of the class, which is also responsible for constructing objects, and in something like JavaScript, they are more separated. I personally find the class-based mental model more intuitive but not sure if that's because I encountered it first or if it's truly a simpler mental model for the way my brain works.
Re: composition over inheritance, I sometimes worry when this is expressed by some dogmatically. My experience is that sometimes I wind up refactoring composition into inheritance to improve the system, and sometimes I refactor inheritance into composition to improve the system. I think if unsure what to do and choosing composition to start with, that totally can make sense. But when I see it expressed dogmatically I believe this to be unwise from my own personal experience. I like that your article clarifies how everything is trade-offs towards the bottom and this is one of those things where there are trade-offs, even if one is viewed as a best practice by many.
While I'm a huge fan of OOP and Ruby is my bread-and-butter language, I do empathize with the learning-curve of OOP. Some of these concepts do take a bit of time to click for many. One thing I've noticed that can cause a bit of confusion for folks new to the concepts re: inheritance is knowing how to traverse the hierarchy to find what method will be used. It seems like in many OOP paradigms, you follow some class reference, at least conceptually, and from there the classes are linked by super references, at least conceptually. So just understanding that for finding most things you start at a "class" concept and from there you switch to a "superclass" concept I think can cause some awkwardness in reasoning about a system to the uninitiated. Obviously, other paradigms can have their own learning-curve challenges, but this is one that I think I would mention if I were trying to criticize OOP programming, even though it's not a challenge I experience, personally, as being among the initiated.
I enjoyed the article! Good stuff!
Similar to you, my first introduction to object oriented programming was with C++ with those 3 core principles (inheritance, encapsulation, and polymorphism).
As I got older and learned more history. Alan Kay said: "I made up the term 'object-oriented,' and I can tell you I did not have C++ in mind".
From what I've gathered, Alan Kay really had actors with message passing and extreme late binding in mind. Inspired by a language for simulation. I think things like erlang actors or even RESTful APIs are definitely more the objects he had in mind. Objects were these things that existed that you could send messages to and ideally they ran concurrently. My understanding is that the common mapping we have today of "method equals function called on the same stack" was initially done simply due to hardware limitations. In this model it's clear why Objective-C is a more OO language than C++. Extreme late binding with introspection. You can at runtime decide which messages to respond to.
Of course that's not to say Alan is right about how to build good software. But he does get a say in the definition of the phrase he popularized ; ).
In my opinion, inheritance really has nothing to do with object oriented programming. It's one of a few strategies (like mixins) to organize and share code.
Polymorphism is a useful property that absolutely applies to object oriented programming. It sort of comes along with extreme late binding when you say "hey, I have a Account object that responds to a fundsAdded message, but I want to have an audit trail so I'm going to give you a different object that responds to a fundsAdded message by generating an audit log and then delegating to the original object but you don't need to care about that."
Encapsulation similarly falls out of a model of "I just send a message to something and it does something. How would I even know it has a private bool of internal state I shouldn't touch. Obviously I shouldn't touch internal state that's why I'm sending it messages using object oriented programming.
Regarding the Alan Kay quote, in May of 2017 he qualified the C++ dig in an email to me:
First, C++ was actually inspired by Simula -- which had several early great ideas -- and was implemented as a preprocessor to Algol ,plus storage management for Simula "objects", which were free Algol blocks. Bjarne Stroustrup's plan was to do something very similar on top of C. He was not trying to do a system like Smalltalk (and certainly didn't get one).
A decade+ into my software development career, I admit that OOP still confounds me.
I have developed a few assumptions about OOP:
I tried to design a small OOP program along these lines, and ended up with this code, which is eerily similar to OCaml module approach. Appreciate if someone proficient with OOP paradigm can critique it.
Consider encapsulation as an inevitable consequence of the receiver being far away from the origin of the message. Perhaps the receiver is in another process, in another VM, or on another machine on the other side of the network. Perhaps the receiver is on the other side of a security boundary, or an intrinsic/builtin operation, or fundamentally not trusted with implementation details. Encapsulation allows us to ensure that there is one uniform calling convention.
Your code is fine. Java will be limiting. ais523 and I discussed this yesterday and I ended up writing the following about class-based languages (posted here):
While object-based languages ideally allow delivery of any message to any object, possibly failing at runtime, class-based languages often reject this. Instead, for every class, there is a type of messages which may be delivered to that class. Languages like Java or OCaml integrate this restriction into more holistic type systems which treat classes as types.
Consider encapsulation as an inevitable consequence of the receiver being far away from the origin of the message. Perhaps the receiver is in another process, in another VM, or on another machine on the other side of the network. Perhaps the receiver is on the other side of a security boundary, or an intrinsic/builtin operation, or fundamentally not trusted with implementation details. Encapsulation allows us to ensure that there is one uniform calling convention.
I agree with this. But I feels like the class-level atomicity of encapsulation as employed in class-centric OO languages feels uncomfortably low-level to me.
I agree about Java limiting what's possible with OOP. That's one of my qualms with OOP - my understanding of OOP does not really map to the mainstream (i.e., money making) OO languages. I comfortably understand OOP as perceived in languages like Scala, Erlang, Elixir, etc. But I don't feel that I "get" the mainstream OO languages. I would certainly be able to come up with a workable program in Java, but I wouldn't feel it well designed.
Sorry I don't have concrete thoughts to offer.
This was a good read. I'm a little surprised that there's not a distinction between object-based languages where all referents are objects and object-oriented languages which also have some non-object primitive values. I'm disappointed that capabilities weren't mentioned, but I understand that it's a minority view which most surveys ignore.
Message passing gains a Renaissance in concurrent programming, ironically through non-OOP languages like Erlang and Golang, with constructs such as actors and channels.
Dart, Scala, and Swift come to mind. Plenty of object-based languages insist that every object is its own actor; languages like E and Pony have security models based on encapsulated actors.
[Open recursion] just describes a familiar property of object-oriented systems: methods for an object can call each other, even if they are defined in different classes in the inheritance hierarchy.
This is one of the insights that allow functional pearls to be transliterated to objects. Objects are known to be good at representing closures; when a function needs a closure, turn the function into a method and embed the closure in the instance state. Similarly, self-referential objects are good at representing letrec; whenever there is mutual recursion or a letrec, turn each entrypoint into its own method and attach all of the methods to a single object which can refer to itself.
No reason to "close" a non-public module where you own its usage.
Maybe it was already closed. I've had situations where there is a module or object in immutable storage and I want to extend it. In this situation, the open-closed principle naturally arises if we think of my extension as occurring in code which is open to mutation and based on code which is closed to mutation. Example media include CD-ROMs, content-addressed media from The Network, build artifacts in a Nix store, and primitive objects built into the VM itself. Multi-stage programming also can be thought of as naturally opening code when quoting it and closing it when splicing. In both cases, to "own" the "usage" of an object isn't enough, although it's required to put code into a quote or onto a CD-ROM; the precise details of when and where the object is located in spacetime must be included.
I'm a little surprised that there's not a distinction between object-based languages where all referents are objects and object-oriented languages which also have some non-object primitive values.
Then you have current state of Common Lisp, where everything counts as an object, but not every standard function is a multimethod…
I'm disappointed that capabilities weren't mentioned, but I understand that it's a minority view which most surveys ignore.
Object capabilities are awesome! But they have next to nothing to do with OOP, and just calling them "capabilities" is IMO clearer.
Three things I would add to this list: Objects, Prototype and Delegation OOP languages, and Multiple Dispatch.
In an object-oriented language, data is normally represented by a possibly cyclic graph of objects, which point to one another. An object has state, which is often mutable (but can be immutable), and regardless of this, an object has a unique object identity which is preserved over state changes. This is distinct from pure functional programming languages, in which data is represented by immutable values, which do not possess object identity.
Examples of language that represent data using objects: Lisp, C, Python, Javascript, Ruby, almost all mainstream languages.
A language is not "object oriented" solely because it uses objects to represent data. According to Alan Kay's original definition, you also need encapsulation, message passing and late binding, and all of today's languages that advertise themselves as "object oriented" have all these things. Despite what the author says about Kay's definition(*).
(*) I understand Kay's text as defining "Object Oriented Programming", which is a style of programming where you represent all state as encapsulated objects, and all computation is initiated by sending messages with late bound semantics. Kay doesn't define "Object Oriented Language" in that text. I personally take it to mean a language that supports OOP. Maybe people who take issue with Kay's definition are getting hung up on purity. Smalltalk is a pure object oriented language, OOP is the only style of programming supported, whereas modern OOP languages typically are not pure and support multiple paradigms.
Kay doesn't mention classes and inheritance in his definition. The thing is, to make OOP practical, you need something very much like classes and inheritance. So it's a second order requirement implied by Kay's definition. Smalltalk 72 had classes but not inheritance, and to define multiple classes supporting the same protocol, you ended up copying and pasting a lot of code. This copying and pasting is described in the Smalltalk 72 manual as a design pattern. Classes and inheritance, in the specific form seen in Smalltalk 80, are not essential to OOP: this is demonstrated by the Self programming language, which uses Prototypes and Delegation to accomplish the same goals in a similar but not identical way. Self is always described as an OOP language.
The final thing missing from Fifty Shades of OOP is Multiple Dispatch. The CLOS (Common Lisp Object System) is a generalization of object-oriented programming that supports multiple dispatch, rather than just single dispatch. Some of the terminology changes. Instead of "message passing", with it's special syntax, we have calls to "generic functions", which syntactically look like ordinary function calls. This change in syntax doesn't disqualify CLOS as OOP, you can do the same things. There are methods, classes, and class inheritance, and the idioms of object oriented programming are supported, so CLOS is "object oriented programming".
Following the discussion from https://lesleylai.info/en/fifty_shades_of_oop/, this article has a lot of references and points.
One reason that language designers often try to avoid subtyping is the implementation complexity. Integrating bidirectional type inference and subtyping is notoriously difficult. Stephen Dolan’s 2016 thesis Algebraic Subtyping makes good progress addressing this issue.
This seems to be somewhat incorrectly stated. Bidirectional typing can be used as a solution to the issue of combining subtyping with type inference, by allowing the programmer to add some annotations to guide typechecking, and resolving ambiguities on a case-by-case basis.
The really tricky thing is combining type inference with principle types and subtyping. This is what Algebraic Subtyping addresses. But most languages don't need or want principle types (i.e. where you can remove all type annotations and still infer a most general type for every expression).
There is also this great talk by Casey Muratori about the topic: https://youtu.be/wo84LFzx5nI as well as an article with more background information: https://www.computerenhance.com/p/the-big-oops-anatomy-of-a-thirty
One thing I personally like "ragging on" OOP for is structural inheritance (cue being told that's not OOP). It just makes me so sad when a subclass, which has a better defined and thus usually narrower use case, is larger in size than the superclass. Say that again with me, slowly:
The subclass is larger than the superclass.
As a concrete example of how this plays out oddly in practice: let's say you're modelling a UI of components, built in JavaScript using a nonreactive model, ie. Backbone or similar. Your base class has properties for the position and size of the object, and this base class is directly used to group components together. Child classes are then things like text or image.
It feels like the position and size make sense for all of these, until you realise that text might actually freely overflow its bounds and image is unlikely to change them ever. The base class when used as a group considers those properties to be its most important feature and accesses them regularly (eg. when children change), but the child classes may ignore them entirely or access them only very rarely. For child classes it would thus make much more sense if these properties were defined outside of the main class, in some "cold data" structure. But substructural inheritance has kind of ruined this for you (assuming you use the class keyword and property syntax).
Say that again with me, slowly:
The subclass is larger than the superclass.
A class describes a collection of values: every object which is an instance of that class. The conceptual "size" of a class is roughly how big the universe of values it contains is. "Subclass" and "superclass" make sense then. Every instance of a subclass is also an instance of its superclass(es), so the set of values in the subclass is a smaller set than the set of values in the superclass, which contains all of the subclass's values and possibly others.
It makes sense that an instance of a subclass has (potentially) more state than an instance of the superclass. An instance of the subclass is an instance of the superclass, so it must have everything a superclass instance needs. But because it is also a specialization of that superclass, it may have more things that only it needs.
The base class when used as a group considers those properties to be its most important feature and accesses them regularly (eg. when children change), but the child classes may ignore them entirely or access them only very rarely.
While methods implemented in the child class may only rarely refer to those fields, inherited methods from the base class are valid operations on that child class too. So if those base class methods (which are called on instance of the child class) are called frequently and use those fields, then the child class is certainly deeply reliant on that state.
If you have base class methods and state that instances of a subclass almost never use at all, then it's likely a sign that you don't have a good inheritance hierarchy. (Which is common, because defining good inheritance hierarchies is harder because code reuse is hard.)
A simpler way of saying this is: When one narrows a type, the resulting type may have a larger surface area, but never a smaller surface area.
A class describes a collection of values ... it must have everything a superclass instance needs.
Is the OOP definition of a class truly "a collection of values"? That sounds more like a struct. I would argue that the existence of any values behind a class interface is intentionally hidden or encapsulated, and that a class is rather a collection of methods or interactions on an instance. In the technical sense, yes, a class generally is a collection of values but it need not be and it does not need to have everything that a superclass instance needs. It only needs to be capable of producing said data (and likely persisting it if stored) upon request. This does require that the base class data is accessed through getters and setters, though.
An example of how to do this would then be done is for a subclass to hold an initially null pointer to a superclass instance. Whenever the subclass is queried for data of the superclass, it will return the default value without touching the pointer (though it is likely still read into cache due to being on the same cache line). If data belonging to the superclass is mutated, then a new superclass instance is created and the subclass points to it; the mutation is then passed onto the new instance. This way we've made the subclass potentially smaller than the superclass without any loss in features, and if superclass data mutations are rare then we've saved memory. Importantly, we've saved the child instance's cache line from being polluted with rarely used superclass data.
If the cost of a pointer in the subclass is too much, you can also store the pointer in a hash map on the side using the subclass pointer as a key. You'll also want to store a single bit in the subclass to indicate whether the hash map contains data for a subclass instance or not. This way you've saved even more memory and cache line, at the cost of worse performance on superclass data lookups and mutations (when a superclass instance has been created).
To give a concrete example of why this might be useful, I'll refer to JavaScript engines since that's kind of my expertise. An ArrayBuffer in JavaScript is an object used to store a buffer of bytes; it cannot be used to access the bytes at all, you need a "TypedArray" or a DataView for that, but it acts as a sort of handle to the buffer and can be used to resize or "detach" (sort of like deallocate but not quite) the buffer. An ArrayBuffer in JavaScript usually appears for that express purpose: to act as a handle to an allocation. But an ArrayBuffer is also a JavaScript Object, so internally engines have it store two (arguably three) pointers worth of memory for the Object features. These features are exceedingly rarely used: storing properties or changing the prototype of an ArrayBuffer has no effect on its allocation, and the allocation is what you're interested in. Those two pointers in the ArrayBuffer instance are effectively just wasted space, technically necessary but in actual reality entirely untouched by user action.
This is probably a sign of ECMAScript not having a good inheritance hierarchy, but knowing that its bad doesn't really help your browser's memory usage. Not using structural inheritance would.
It just makes me so sad when a subclass, which has a better defined and thus usually narrower use case, is larger in size than the superclass
I find your reaction to larger subclasses odd. I can image an I/O class that has two methods: read() and write(). An instance of the base I/O class would act like /dev/null, and it's the subclasses that implement all the gory details of the particular piece of hardware (real or virtual). I don't find that unusual.
That's pretty close to an abstract base class, and it does indeed make sense that if you have no data then you're bound to be smaller than a class implementing the same API.
But say the /dev/null I/O base class was implemented by actually opening a file handle to /dev/null: does it still make sense for the child to be bigger than the base class? Or could there be a world where the child removes all of the parent data as unnecessary? What if the data is not fully unnecessary: maybe the child class has a "clear" method that actually uses the base class' /dev/null handle to flush its own hardware I/O. Does it now make sense for that handle to always exist there in the child class memory? What if that method is used only very rarely, perhaps even only once during the runtime of the program: does it still make sense?
The nasty part about structural inheritance (especially with a vtable pointer) is that as a subclass your most important data is very likely the data that the subclass has added but it is as far from the base pointer of the object as possible since all parent class data comes first. This means that 1. when you operate on the child class data (the likely scenario), you're much more likely to have to load in two cache lines (one for the vtable, one for the data), and 2. when you operate on any of this data, you're much more likely to find that most of the loaded-in cache lines contain data you're not going to touch as it is either entirely unused or only relevant on some rare call paths that your subclass instance may never even see.
This is why I find structural inheritance sad: it can easily thrash cache lines something bad.
I'm talking actual device drivers here. You know, an OS based around OOP principles. Should the base I/O class contain a buffer (or pointers) for input and output? Not all devices have both input and output. A "character" device might not support a seek operation, but block devices do. Only some devices can change speed, not all.
The nasty part about structural inheritance (especially with a vtable pointer)
vtables is a implementation detail; it's not a requirement for OOP. Also:
it can easily thrash cache lines something bad.
That's a hardware detail. Granted, it's prevalent now but was not always true, and is again, an implementation detail that has nothing to do with OOP. And I'm sure that in 40 years time, programmers will be bitching about the decisions we made today and how stupid we all were, especially with all the "cache coherence" and "async I/O" craziness.
Should the base I/O class contain a buffer (or pointers) for input and output? Not all devices have both input and output. A "character" device might not support a seek operation, but block devices do. Only some devices can change speed, not all.
It kind of sounds like these devices shouldn't actually even be related to each other in an inheritance hierarchy. I guess this is what they call the domain model appearing in the engineering: if we have one device that only supports input and another that only supports output, then it sounds like there is nothing relating them to one another.
I'm talking actual device drivers here. ... is a implementation detail ... That's a hardware detail.
And I'm talking about structural inheritance, and about cache line effects. Yes, neither vtables nor cache lines have anything to do with OOP (the latter especially! :D ), but in the world I live and work in right now, right here vtables and structural inheritance are very strongly correlated with OOP, and real actual hardware is the thing that actually runs the software I write. The thing I am "ragging on" is structural inheritance and how bad it is for cache coherence, but by casting that stone I will always hit OOP so I don't try to needlessly hide that this is a ding towards OOP.
And I'm sure that in 40 years time, programmers will be bitching about the decisions we made today and how stupid we all were, especially with all the "cache coherence" and "async I/O" craziness.
Absolutely: nothing is permanent except change. Yet, I personally still think it's important to write software with a consideration to the hardware that will run it (and the amount of RAM in an average user's pocket) even if in 10 years that turns out to no longer be strictly necessary.
A fun example from my own work life: some years ago I was looking at a data structure that my friend's father had written back in the 70's and cursing him for choosing to not make it a struct containing the data (a bit) and the errors (a 16-bit unsigned integer) like all the other data structures of this variety were. He'd instead chosen to store the single bit inside the errors, halving the size of the structure at the cost of not being consistent with its "siblings". I went and asked him why he'd done it, and he (somewhat sheepishly) explained that at that time they were still worried that there might not be enough memory on the system, but in a few years time that worry had turned out to be entirely unnecessary and memory was nowhere near of running out. But! Looking back on it now with fresh eyes and thinking of CPU caches, I think he absolutely made the right call. No, memory still isn't going to run out, but wasting 16 bits is still waste and would have caused bad cache coherency: when you can, you should absolutely aim to not waste the hardware you've been given.
Re: some of the discussion here. I wonder if we'd find any answer whatsoever if tried to define what is not object-oriented programming?
The article says:
Personally, I feel that prototypes are harder to wrap one’s head around compared to classes.
and also
When I learned JavaScript, prototype inheritance and the semantics of this were two of the topics I struggled with the most.
I don't think prototypes are notably harder than classes, but the way JavaScript treats them obscures the essentials quite a bit, and makes it (much) harder than necessary. The prototype is hidden away as a property on the constructor function. And then the function can only be called with new to actually get an object (which can be confusing to people who already know class-based languages). And this binds in an awkward way and can get "lost" when calling the method as a function directly. There's no reason any of this has to work like that.
What does OOP have to do with BDSM-themed Twilight fanfic?
Death of the author
Neither Alan Kay nor 50 Shades' author are dead?
Alan Kay is alive???
The header mentions that to use the formal definition requires a definition of object, and then just doesn't do so, even though it's easy and it unlocks what everyone's complaining about. People complaining about objects are complaining about the design paradigm; talking about all the language features and giving a brief mention to some components of the design paradigm but never once addressing the design paradigm itself despite starting off with an acknowledgement that it exists is plain silly. You can feel like other things are more important and even give those pride of place, but if you're going to talk about the exhaustive meaning of greeble-oriented frobnicating and never actually mention what a greeble is and why it matters, it is impossible not to describe the article as incomplete.
Is there a commonly accepted definition of an object in the first place?
If there is, it's probably one that Alan Kay will yell at us about not being the true definition.
They teach you it in the first class after the basics, in university. An object is an agentic entity. It has both state and behavior, but you cannot view its state except through its behavior. It has individual responsibility; you do not think of the caller as doing things with it, but rather as telling it to do things. It is defined by its interface; another object with a different implementation thereof can be swapped for it without you noticing. It has no particular location and no particular lifetime; it is an entity that you talk to. It is, in other words, almost exactly like a web server or daemon. Everything in your codebase that ends in 'er', like ThingManager or FileReader, is probably an object, even if you're using Haskell or C or some other legendarily-unfriendly-to-objects language.
This is why, by the way, everyone thinks there's some great disagreement between Smalltalk and Java and that's why there's no one true definition because everyone disagrees with Alan Kay who invented it. The misapplication that Kay repeatedly complained about was about missing the forest for the trees, thinking of class inheritance rather than objects' conceptual role; reducing it to message passing is about as wrong as reducing it to virtual inheritance, even though message passing is hard to implement without getting close to the mark.
They teach you it in the first class after the basics, in university.
because everyone disagrees with Alan Kay
I'm not sure why you think the definition you were taught in university is universal. Universities teach different things, especially US vs. European universities, and it's not like there's a standard curriculum. This thread is full of people arguing over the definition of "object". I count at least four very different definitions: (i) Alan Kay & Actors, (ii) William Cook and "encapsulation" / dynamic dispatch, (iii) everything's an object including closures and modules, (iv) your fuzzy definition about behavior and interfaces. These definitions are truly distinct, in that they disagree about whether a particular value in a programming language is an object or not.
i, ii, and iv are the same concept in different words (or if you prefer, the same philosophy with different aspects emphasized). iv is the most clear, but that is not Kay's fault, because web servers did not yet exist as a popular concept for him to describe it in terms of (though he tried to popularize such a concept by describing it in terms of objects).
iii and its many contemporaries are derivative of a basic mistake: being taught object-oriented programming philosophy as fact initially, learning to think about programs through the lens of object-oriented programming, not being taught what the structure and bounds of the philosophy actually are in so many words or how to think through a different lens, and guessing at a workman's definition by just listing off the aspects of what they have learned that stand out to them, like virtual inheritance. 'How to think' is the most important aspect of all, does not stand out to you because patterns of your own thought never stand out to you, and can only be understood with a concrete frame of reference to something else and an explainer to highlight which parts are important. It is like fish trying to come up with definitions for wetness.