Pitfalls of object oriented programming (2009) [pdf]
harmful.cat-v.org
harmful.cat-v.org
1. https://www.youtube.com/watch?v=VAT9E-M-PoE
2. https://docs.google.com/presentation/d/1ST3mZgxmxqlpCFkdDhtg...
(In a way, even CSS is kind of OOP, albeit with very confusing overriding rules.)
When writing generic backend code in a language like Java, I often found fighting against OOP because the business logic is not necessarily mappable to "dog inherits from animal". And I can't change the business logic, so OOP has to give in. But when developing user interfaces, I am usually in more control, I am actually designing it, not observing directly from the business logic. And OOP forces me to have a more consistent implementation, which is beneficial for the user, because consistent UI makes it easy for people to get used to it, find the underlying logic in it, and start relying on it. If all links on all web pages would inherit from normal HTML href link (like it was designed in the beginning), we wouldn't suffer from those weird JS web apps where you can't open a link in a new page for example.
> there could be an element inheriting low-level behavior (HTTP/API call, etc) from a button and a high-level behavior (style/presentation, etc) from a visual class.
This becomes very messy very quickly, and you would need to clearly separate the concerns to make them non-expressible on either side. CSS and JS make this possible by being two different languages, one Turing-incomplete (in usual scenarios).
You avoid duplicated code by making generic functions, putting them in modules, and making your objects accept them as parameters.
Regardless, the inheritance remains an implementation detail.
[1]: https://docwiki.embarcadero.com/RADStudio/Alexandria/en/Inte...
I admit I would really like a 'delegate function call to <object>' construct, but excuse me for being dense, what composition are you referring to? Other than inheritance and delegation, what else is there[1] , and what would a suitable construct supporting it in the language look like? TIA
[1] (there is functional-style composition, but that's perfectly well supported in many decent languages like Scala)
Yeah, I'd love to see Kotlin's delegation[0] developed further and appear in other languages as well.
[0]: I admit I would really like a 'delegate function call to <object>' construct
One reason is that inheritance is strictly more powerful. When you delegate (compose), you can't modify the inner working of the object you delegated to, you can only wrap it. Often that's not sufficient. You want to actually customize the implementation in some way. OOP's inheritance is ideal for this, because the superclass can clearly define chunks of functionality that subclasses are allowed to either (a) replace entirely or (b) wrap or (c) provide (abstract methods).
This is a very flexible approach. With mere delegation you can only wrap, and only for method calls that originate outside the object, and only for public methods.
It's also clean. The distinction between what you must implement, what you can implement and what is for the user is defined in the type system and enforced by the compiler. Additionally you separate the interface for customizations from the interface for users (protected vs public), which keeps documentation and auto complete properly focused.
When combined with dependency injection and code generation it also allows third party frameworks and libraries to enhance the code you write in various ways, that remain mostly tucked out of the way but can be revealed in an IDE in an instant (and a good IDE will show visual hints that there are such customizations in effect).
The final advantage is that in some languages the implementation is highly optimized. On HN you'd be forgiven for thinking nobody writes virtual method calls since 20 years ago but in reality virtual method dispatch is so common in real programs that it's heavily optimized by both CPUs (sophisticated indirect branch caching) and language runtimes (PICs in fast VMs like Hotspot or V8). Composition is less well optimized.
Inheritance gets an overly bad rap on Hacker News, despite how prevalent it is. I think this is because it's so flexible you can easily make a mess with it, and some people have. Also the decision of Java to make all classes and methods open by default means overriding is often used as a hacky way to hot-patch libraries in the field, rather than as part of any principled overall design. Kotlin reverses that decision to some extent and so classes and methods are all final by default, unless a compiler plugin overrides that.
I also suspect the move to web development has had this effect, because in most classical OOP toolkits you are allowed or even encouraged to extend the UI by subclassing pre-existing node types. So you can make a custom button by subclassing Button or whatever. HTML is implemented using classical OOP designs, but mostly for implementation reasons that isn't exposed to the web developer who instead is expected to customize the controls purely through CSS and setting properties. If you want to make a custom button widget, well that's too bad, better learn React or something similar. So people just aren't exposed to the situations where OOP is useful because browsers weren't designed for UI, and the memory of why it was originally developed atrophies.
And some of it is just memeing.
OK, now tell me why I'm wrong.
> Often [delegation] not sufficient. You want to actually customize the implementation in some way. OOP's inheritance is ideal for this
The usual way of customising behaviour without inheritance is to pass in an object to actually implement the behaviour.
var LogToConsole = new logger(new WriteToConsole());
vs var LogToFile = new logger(new WriteToFile());
Which customises the logger class. Trivial example but I use this a lot.> I think this is because [inheritance is] so flexible you can easily make a mess with it, and some people have
agreed
C++/Java/Kotlin style OOP is basically about putting common design patterns into language syntax. You could do it all with C, but it's easier if the compiler does it for you. For example Kotlin has "data classes" and Java now has "records" which are in both cases classes, but they sacrifice inheritance in order to auto-generate more boilerplate for you. That's still OOP though.
It's more powerful, sure, but in the same way that goto is more powerful than if/for/while. In theory some languages give you the tools for a superclass to expose a stable interface to its subclasses (ish - even Java doesn't give you a way to specify "subclasses may read but not write this field", for example), but in practice they're rarely used (Wicket is literally the only library I've seen to offer an effective inheritance interface); the overwhelming majority of the time inheritance ends in fragile base classes and the resulting bugs.
Which is perhaps inevitable; the trouble with giving you the tools to change the superclass's behaviour in unintended ways is that it results in people changing the superclass's behaviour in unintended ways. If you have a locked-down inheritance model where classes and methods are final by default then maybe you avoid the fragility problem - but by the same token if subclasses can only use extension points that were deliberately exposed then you're probably not gaining much flexibility over what you could do with composition.
> The final advantage is that in some languages the implementation is highly optimized. On HN you'd be forgiven for thinking nobody writes virtual method calls since 20 years ago but in reality virtual method dispatch is so common in real programs that it's heavily optimized by both CPUs (sophisticated indirect branch caching) and language runtimes (PICs in fast VMs like Hotspot or V8). Composition is less well optimized.
Shrug, as the language design consensus shifts, the implementation optimizations will follow. Composition should ultimately be easier to get better performance out of, because it's more constrained (in particular, knowing that a given class or method is final means that you can optimise that class or method much more aggressively).
> So people just aren't exposed to the situations where OOP is useful because browsers weren't designed for UI, and the memory of why it was originally developed atrophies.
AIUI the association between OOP and GUI is more of a historical coincidence than a deliberate design. Though I agree that they do seem to fit together well.
I think I know what you mean, but you can easily define a private field with a protected get method which amounts to the same thing.
In Kotlin there's no distinction between properties and fields, so you can write:
open class Foo {
var thing: String
private set
protected get
}
to express that.> in practice they're rarely used
I'm not sure I agree. Most Java libraries use it, and it seems to work fine. Mobile UI has used it for many years and that's an absolute ton of code. Game engines use it, games are a huge chunk of the software industry. People would use it a lot more if the web worked differently or if we had better non-web distribution systems on the desktop.
> if subclasses can only use extension points that were deliberately exposed then you're probably not gaining much flexibility over what you could do with composition.
You can certainly do everything that inheritance allows without support for it in the language, but that doesn't say much. You can also do OOP in C, people do, but it's generally more convenient to have compiler support. Used properly, inheritance can certainly lead to much more convenient APIs and code without being prone to misuse.
As an example, one of the issues I have with Jetpack Compose is that if you want to customize the controls in any way you have to wrap them. There's no way to say "I want a MyButton that's the same as Button except in this one aspect", because OOP is gone. You have to define a totally new MyButton function/composable that delegates to Button. In JS it's just about tolerable due to the dynamic typing and related features (e.g. spread operator), but in Kotlin it doesn't work despite relying on compiler plugins that change the language's core semantics. You end up with huge screenfuls of boilerplate which are just forwarding function arguments from one place to another, and if you upgrade to a new Compose do you get the new features automatically? No! Due to its browser heritage React conflates widget libraries with theming, so you've had to wrap all the widgets to get anything beyond the basic Material Design look, and that's manually generated boilerplate so you'll have to go and plumb new properties through by hand. There's also no nice way for the authors of Button to define properties that are only intended for customizers vs users because there's no concrete notion of customization at all. Ugh.
Now compare to an OOP toolkit:
class MyButton extends Button {
...
}
You upgrade the base libraries and MyButton now has the new features automatically, no code changes required.> knowing that a given class or method is final means that you can optimise that class or method much more aggressively
Virtual method calls are treated as final when possible in good language VMs anyway, so it makes no difference. They're only emitted as virtual when there are actually multiple receivers observed at that specific call site, which would have to be implemented as a switch{} in composition, so again, you don't gain anything but do lose language convenience.
It's not though, a method is significantly more cumbersome. People get lazy and make it a protected field, and probably they themselves remember that the subclass mustn't change it and it's only the poor next person taking over maintenance who gets bitten.
> Most Java libraries use it, and it seems to work fine.
I mean specifically offering a restricted interface for inheritance by making extensive use of "final", making unstable fields and methods inaccessible etc.. In my experience libraries are rarely careful enough about that, and upgrading a library that you've subclassed from is usually a fraught process. (Wicket is literally the only such library that I've ever been able to fearlessly upgrade)
> Used properly, inheritance can certainly lead to much more convenient APIs and code without being prone to misuse.
I'm genuinely not convinced. I think if you have interfaces and delegation support, then a well designed non-inheritance API (which might involve e.g. splitting what were single classes in the inheritance version into two) can be just as convenient.
> You end up with huge screenfuls of boilerplate which are just forwarding function arguments from one place to another
Isn't the whole point of delegation as a language feature to avoid that? Can't you just say "delegating this interface to this member" and then all the methods you didn't specifically define get forwarded?
> You upgrade the base libraries and MyButton now has the new features automatically, no code changes required.
Or it breaks, because the base library changed in a way you didn't expect / didn't take into account the way you've subclassed it.
that grossly exaggerates it. It's annoying but nothing like that.
Yes but Kotlin delegation is implemented for objects and interfaces, not functions. Compose is React inspired so it uses functions with keyword arguments for everything, so if a new keyword argument is used, you have to manually forward it.
> Or it breaks
It can happen for any kind of API upgrade, especially where you can customize large amounts of complex logic. I don't think it's unusually worse for inheritance based designs. Obviously that's predicated on there actually being a design, but that's not a problem unique to Java (e.g. C libs that export non-stable symbols is a perennial issue).
Because OOP is somewhat equals to inheritance (or more strictly late binding + open recursion + self-polymorphism, inheritance is just one way to achieve that).
"delegating" is completely orthogonal to OOP, you can have delegates in any modular language, like ML. Saying "you need to use more delegates and less inheritance" basically equals to "you need to write less OOP code".
I'm struggling to understand this, could you give an example?
module type Repo = sig
type t
type user
val get_user : t -> name:string -> user option
val get_all_users : t -> user list
end
module LoggedRepo (R : Repo) : sig
(* Resulting module is of type Repo plus create function *)
include Repo
val create : delegate:R.t -> logger:Logger.t -> t
end = struct
type t = { delegate : R.t; logger : Logger.t }
type user = R.user
let create ~delegate ~logger = { delegate; logger }
let get_user repo name =
Logger.debug repo.logger "Get user by name";
R.get_user repo.delegate name
let get_all_users repo =
Logger.debug repo.logger "Get all users";
R.get_all_users repo.delegate
end
In languages like C it would be harder since they don't have notion of interface and implementation, so you would have to implement that as an object-like structure of function pointers and opaque structs for state.The point is, for delegate you don't need lately bound methods and virtual dispatching, i.e. oop, only the notion of interface and implementation supported (or emulated), and then you can create implementations that wrap another implementations.
module HttpHandler (R : Repo) : sig
val handle_user : R.t -> HttpWriter.t -> username:string -> unit
end = struct
let handle_user repo writer ~username =
match R.get_user repo username with
| None -> Writer.write_status writer 404
| Some user -> Writer.write_body writer (User.to_string user)
end
and then to construct a handler module with a proper repo.That's because inheritance isn't really elsewhere, making it uniquely an OO concept (though I'm sure there are exceptions).
When people say OO is all about X, Y, and inheritance, well, X and Y are in most of the other languages too.
That's what OO is to me. Inheritance is nice for some things, but entirely optional.
Textbooks have an example like "Dog inherits from Animal" as a way to demonstrate the concept of inheritance, but people took that as an invitation to become Linnaeus and create a hierarchical taxonomy of their domain.
It might be a useful exercise, actually, but it's rarely the best way to design software and you can do OOP (and do it better) with sparing use of inheritance (by replacing it with extensive use of composition).
In my experience, a relational model is the best solution for domain modeling precisely because it can directly represent circular dependencies without any sort of weird interface hacks, one off Init() methods, etc.
A practical example of this would be something like the domain of banking. In a bank, you generally have 2 major types of entities: accounts and customers. Accounts can have many customers and customers can have many accounts. Which type should "win"? In SQL you don't have to make this decision at modeling time - it is deferred to the caller and its specific view of the world.
You can certainly emulate this in OOP, but then you are stuck doing joins between collections manually.
What about cycles?
For example, "List all distinct customers from all accounts that are owned by customers who are members of some other subset of accounts".
I tried it with ChatGPT and it did produce a robust solution. This does not seem to be a limitation for strong type hierarchy after all:
```
# Define a struct for Customer struct Customer
id::Int
name::String
# Other customer-related fields
end# Define a struct for Account
struct Account
id::Int
balance::Float64
type::String # e.g., "Savings", "Checking"
# Other account-related fields
end# Define a struct to manage the many-to-many relationship
struct CustomerAccount
customer::Customer
account::Account
end# Define a struct for the Bank which holds all information
struct Bank
customers::Dict{Int, Customer}
accounts::Dict{Int, Account}
customerAccounts::Vector{CustomerAccount}
end```
Indeed. It produced a relational solution that emulates what you'd do with SQL tables.
This is approximately the schema shape we use in our product today.
("We recommend defining components as functions instead of classes." at top of page at https://react.dev/reference/react/Component)
(This change has little to do with OOP vs functional. React calls them function components, not functional. React has always used functional patterns, even in the class components.)
Although React tutorials start by saying your UI becomes a pure function of your app's state, React components do have encapsulated state. Any time you call useState inside a react function you're doing something that doesn't exist in real FP programs. Additionally React boils down to a way to create DOM elements, which are highly stateful C++ objects exposed to JavaScript, implemented using inheritance:
https://source.chromium.org/chromium/chromium/src/+/main:thi...
An HTMLButtonElement is a HTMLFormControlElement is a HTMLElement (and a ListedElement and a FormAssociated).
The rules for how you write code in React (or Jetpack Compose) violate even the most basic rules of the host language. useState defines or returns what is effectively an object field (it's data associated with that widget), but it looks syntactically like a local variable. It's not a local variable though because its value will survive the exit of the scope. Nor is useState a normal function call because there's a hidden invocation counter used to work out what state slot you want, which is why you aren't allowed to call it inside conditional branches and why loops need special handling.
So in React's "functional style" we can see that you're actually defining objects with encapsulated state, that register event handlers, that export properties and so on. It's just obfuscated to look like it's not OOP.
Honestly for recent simple GUIs I've made for my own in house tools I'm still using JavaFX. Mostly because I always seem to end up needing a good table view, and because I happen to know it. If I were working with it a lot I'd end up maintaining a TornadoFX style set of wrappers but for simple stuff the plain API works OK.
Because I'm weird I'd probably use it for low-traffic web apps these days too, with JPro (jpro.one). Probably not for mobile web, and not for stuff where users are on high latency connections in general, but if the user has a nice big screen and a low latency connection I'd personally find it a lot easier and more productive than dealing with HTML and CSS. The results are indistinguishable for the user if you are careful.
Meh, it's just a monad, hidden in a fake syntax so people don't panic.
> Additionally React boils down to a way to create DOM elements, which are highly stateful C++ objects exposed to JavaScript, implemented using inheritance
And C++ and JavaScript are just ways to run machine code, which is fully unstructured. But all of that is an implementation detail. React creates DOM elements because that's what you have to do to render UI in the browser, not because its creators think the DOM is a good idea.
> So in React's "functional style" we can see that you're actually defining objects with encapsulated state, that register event handlers, that export properties and so on. It's just obfuscated to look like it's not OOP.
It's not OOP - it doesn't have inheritance, most state is (idiomatically) immutable and exposed, and as you noted, React state does not follow the same rules as OOP state. While it's not fully pure functional (I'd actually say the React state model is a novel and interesting thing, that I expect may end up being a language feature in the next generation of successors), it's closer to functional than traditional OOP, and most of the benefits are those of a functional rather than OO style.
Jetpack Compose now, that is a pure implementation of the React concept all the way to the bottom. And their compiler plugin basically makes the React model a part of the language, so you could go take a look at that if you're interested in language-integrated approaches. There's essentially no OOP there and all state is handled with their useState equivalent. But as I write in another post it actually does suffer badly from that decision, in my view. The combination of the React concept with static typing doesn't mix well at all. There's an astonishing amount of copy/paste coding going on in the Compose landscape and worse, a new generation are starting to learn that this is normal and just the way UI code looks.
On the web the React approach took off due to the history of JS/HTML not really supporting proper componentization or indeed proper OOP until very late (arguably it still doesn't). Then the Android guys decided to adopt it to be more familiar to web devs and as a way to justify the huge expense of starting over, which they did need, the old Android toolkit has a lot of tech debt in it.
But I think we will come to regret that evolutionary path. Once you get past the convenience of being able to add/remove widgets by rebuilding the whole layout from scratch, it's just not a good way to express UI in statically typed languages.
For someone writing stuff in React all that is effectively the "runtime", and whether it's being managed in OOP, FP, or some other fashion is an implementation detail. (You don't e.g. subclass DOM classes when writing React).
> The combination of the React concept with static typing doesn't mix well at all.
Hasn't been my experience at all, shrug, my main job does a lot of stuff with React in Scala.js and it works very well.
Though most contemporary frameworks (react, elm, android compose) tend to prefer (functional) reactive programming.
In the 80s we called that "event-driven programming", and until threads became widely available, it is how all native GUI programming worked, more or less.
CSS is also interesting, we mostly try to avoid the inheritance with scoping, utility classes and or css in js solutions as it becomes really hard to manage otherwise.
Inheritance makes for a very powerless form of logical programing. CSS is way more than OOP.
(Powerless is always preferred if it's sufficient, but CSS clearly shows a single hierarchy isn't sufficient.)
The right way to achieve the same and better results is interfaces / typeclasses / traits. They work pretty well in languages from Rust and Haskell to Typescript, Kotlin, Java, and even Go. (Go is a bit special because you don't even have to declare that your class implements an interface, as long as it actually has a set of methods with right signatures.)
Ideally, all public methods should only describe parameters and return values in terms of interfaces, and never as concrete classes.
If you need to take a big class and inherit from it to override one or two methods, it's a failure to compose the things correctly. These one or two methods usually are better off as their own entity which you can pass to the "base" as parameters. This both prevents proliferation of "god objects", increases testability, and improves build times (shallower dependency graph, yay).
Citation needed. I've seen interface heavy Go code, and without fail it's always unreadable garbage. Interfaces are best used in moderation, at API boundaries only.
Well, yes, at API boundaries, as you say, or in public methods, as I say. We agree!
> Prefetching
> Data accesses are now predictable
I'm reminded of a Stroustrup talk in which he says that vectors will always outperform linked list structures, by taking advantage of spatial locality and predictable access patterns to enable better cache prefetching. He even has a slide on C++ vs "True OO" style object graphs, and it looks remarkably similar to what's described in the OP: https://www.youtube.com/watch?v=YQs6IC-vgmo&t=3m53s
> Data abstraction
> Encapsulation
> Polymorphism
> Inheritance
Note that it's the last two concepts which are specific to object oriented programming. The first two are already supported by non OOP languages like C and enable implementation of abstract data types. I think it's unfortunate that all concepts are often introduced in the same OOP course which may make the students believe that a class and an abstract data type is the same thing.It's also worth making a distinction between an abstract data type - a "thing" and some set of operations defined over it - and a data structure - a particular in-memory construction and set of procedures defined over it, often used to realize an abstract data type. An example being a dictionary ADT - supporting get, set, delete, etc. being realizable through different data structures; for example, a linked "association list" of (key, value) pairs vs. a hash table - with each having different performance characteristics, despite possessing the same properties as the ADT they implement.
The term "polymorphism" was first used in the documents of the language CPL (in mid sixties), the source of BCPL, where in British English it corresponded to what in the American documents of the same age, starting with McCarthy, were called overloaded operators and overloaded functions.
The use of "polymorphism" mostly for the run-time polymorphism of OOP languages has appeared only a couple of decades later, with Smalltalk.
"Generic", which might be a better term than both, has been popularized during the programming language contest that has lead to Ada (1979). Nowadays it is mostly used for polymorphism/overloading that is resolved at compile time, because this is how the term has been used in Ada.
I mean, you can do OO type stuff with anonymous handles but the compiler will let a function take one handle type where another belongs and things will fail opaquely but immediately if you're luck and fail slowly if you're unlucky. Which is to say there is no "support".
You simply declare a type as a pointer to a struct in the header file along with the functions that operate on variables of this type, and define the struct and the functions in the implementation file.
M.h
typedef struct M_TDesc *M_T;
void M_Foo(M_T x);
...
M.c #include "M.h"
struct M_TDesc {
...
};
void M_Foo(M_T x)
{
...
}
...Somewhat related, it has been shown that a language with public/private visibility modifiers is more expressive than one without (that is, it can’t be locally replaced by another expression that results in the same program).
Call it what you want but it works well. Declaring a variable or function as static corresponds to "private". I honestly don't understand what you mean by "locally replaced by another expression", I think you'll need to exemplify.
> • What is OO programming? – a programming paradigm that uses "objects" – data structures consisting of datafields and methods together with their interactions – to design applications and computer programs. (Wikipedia)
It isn't that they are together, it's that the core entity of the language used to express solutions centers around identity over state or values. As in: two objects are different even when the fields are all equal, and also: two objects can be the same even with different field values.
If you are going to object to this, I ask that you first contemplate whether or not you have programmed in any other way first, because as a fish, reasoning about what water is can be difficult.
so i feel secure in claiming that i am more a walrus than a fish and therefore qualified to opine about water
i do agree that identity and hidden mutability are important aspects of the oo view of the world, and that the author of these slides is off-base about what oo is. but i think that the essence of oo is that computation is done by sending messages to objects, but sending the same message to different objects invokes different code. this feature is present without mutability in bicicleta, and in abadi and cardelli's untyped ς-calculus, which it's based on; the ς-calculus was invented to formalize the essence of object-oriented typing
the original intuition behind oo was to decompose a computer system into smaller computer systems, like a computer network; each object in the system was in effect a tiny computer, with its own private code and state. this was inspired by alan kay's undergraduate degree in biology, which mostly studies organisms made out of cells, which are basically tiny organisms. and in that formulation, obviously hidden mutability is the default, because you can't prevent a computer from changing its internal state as a result of a packet you send it; and mutability generally means identity is important
but even more central to this formulation is the idea that different objects can behave differently in the same circumstances
obviously none of this has anything to do with memory layout! except very indirectly
If you boil it down to the essence, this means branching logic is encoded via a function pointer. Instead of manually switching to the correct behavior using other branching mechanisms like if or switch.
This is what polymorphism is about. It's pattern matching.
An interface is like an Enum to which you can add entries after it has been defined, and the behaviour for that specific variant naturally then has to be encoded inside the variant itself.
And thus, "more natural" than, let's say, procedural programming.
Animals can be modeled nicely with objects/classes. Physical objects do have internal private state and public interfaces. Also in nature, usually, you have multiple similar objects that work the same but have different internal state.
But even if you're not inspired by biology, it might just come up "naturally". Separating your data into structs, then operations on those structs, and not wanting to deal with all kinds of bookkeeping all the time, letting things manage their own - separation of concerns, aka single responsibility principle.
I think automatic memory management might be a big example of this. Who wants to bother with this malloc/free stuff all the time? Who wants to keep track of ownership everywhere? Instead, have a dedicated subsystem handle it. That allows a higher signal to noise ratio in all the rest of the code that would otherwise be polluted with malloc/free. It also removes the entire class of bugs that can happen when you inevitably forget either of those.
But having memory management in and of itself isn't "OOP". It's just an example of one of the principles that people tout when they talk of "OOP". Those principles can and do apply broadly.
Was Alan Kay thinking of all this? I really don't know. All I know is that the dude digs biology.
https://lists.squeakfoundation.org/pipermail/squeak-dev/1998...
It's a concept I really like and use quite a bit in game development at the inter/intra-entity communication level even if below that I'd pick a more performant substrate.
https://archive.org/details/byte-magazine-1981-08/page/n75/m...
"Design Principles Behind Smalltalk" Byte Magazine 1981
https://archive.org/details/byte-magazine-1981-08/page/n299/...
This doesn't seem like a sufficient definition, as closures are also sub programs with their own state and behaviors that implement a common interface (the function signature), and you'd have to stretch the definition of OOP to the point of meaninglessness if that would be sufficient to qualify as OOP.
[0]: https://handbook.selflanguage.org/2017.1/progguid.html#behav...
It's that the receiver of those messages have an identity that defines it.
Inheritance and polymorphim is of course central to OO, but it's not defining characteristics imho because inheritance is "module arithmetic" and polymorphism has plenty of flavours.
(I am in no way trying to have a go at OO either.)
The defining feature of OO approaches is in shifting the emphasis in sw development into the concept of identity-bags. All the other stuff about message passing, encapsulation, inheritance etc is just window dressing and implementation details of this core concept of identity-classification in software systems.
I like how the "Out Of the Tarpit" (2006) paper breaks this down into the notion of "intentional" vs "extensional" identity, and I think this is a great model.
For them, OO is all about "intentional identity" in that we create identities (objects, components) in our systems and move attributes and behavior onto them. OO shifts the analysis of software to identifying objects, components, services, and then makes software development all about the taxonomical management of them. In this model of software development, it's all about identifying components and then shuffling the data and functionality "into" them and managing the flow between them. (FWIW the paper identifies this way of thinking as one of the prime culprits in unnecessary complexity in software systems, and I agree with them.)
The contrast is "extensional" identity, where identity in the software systems are emergent from their attributes. In this box we can put the relational data model, and its cousins in logic programming (prolog, datalog etc). In those systems, we are not concerned with "creating" identities and classifying systems/components/objects, but instead declaring the raw attributes -- tuples, facts, relations -- only. Queries then produce, at runtime, a multitude of results from permutations of those attributes. And, as you point out, equality/inequality is defined by the value of a tuples attributes, not by the object it lies in. This is crucial.
There's a couple generations of software developers that are so immersed in the OO/intentional-identity way of thinking that they don't see that it's not intrinsic to software development, but actually a mental framework that was fostered through the 80s and 90s. It was an attempt to manage complexity and structure in software systems and also to create a marketplace of re-usable components. But I think it's actually failed at that.
Most classes, objects, components, microservices end up failing in two respects: they end up reflecting the company's org chart more than anything else, and .. worse .. the bulk of the code and complexity ends up being how one moves data between all these (programmer created) systems rather than what the actual data is. The relationships (lines) between the boxes becomes the focus instead of the stuff was shoved into the box, and the whole behavior of the system becomes difficult to reason about.
And when developers encounter systems that don't work along this model -- relational databases, functional programming, logic programming -- they often scratch their heads, resist, or wrap them up/confine them in OO/component type structures... damaging their usefulness.
With that in mind, I do believe that objects, as in encapsulating some kind of side-effect, is a very good model. But it is not a replacement for everything - a more data-oriented approach makes much more sense for some other parts of a program. Multiple views are absolutely possible within the same language/program, and should in general be the preferred way.
So a "class" (and object identity) is really telling the system: when you see attributes X, Y, Z bundled together as a result of a query, here is a set of behaviors that you can apply on those tuples.
Like, I think we should think of a `Connection` as an object, not `User`, which is just plain data.
Maybe structural typing is somewhat similar to what you think of? Probably typescript, maybe go are the most well-known for this out of the statically typed ones, but clojure is also all about every “object” being a key-value map.
And I don't think it's so much structural typing, as it is defining method/function dispatch off predicate matches. More.. structural values? Clumsily:
Disconnect(connection_id) = with project ConnectionState CS where CS.Id = connection_id { if CS.state != 'connected Raise('not_connected) else CS.state = 'disconnected }
I've come to appreciate these difference in later efforts to learn functional programming as a way of giving myself more tools. But for the longest time, it was very difficult for me to "get the boxes right" for what things go where. What is a module, an object, a function, a method, etc, coming from the object oriented universe.
I still have the urge to "create objects" when I conceptualise solutions and _imho_ this isn't always a good path.
These days OO exists within my horizon of tools for solving tasks. Before, it was the water, if you will. Some problems have a want for objects, others don't really.
What OO has become in the industry is a product of practical concerns encountered by people working in the field. Can't say I like it much, but working in this industry for 25ish years, I can understand the pragmatic "whys" of every step of language and tool design fairly well.
In some cases, it’s OOP, in some cases, it might be something like FP, or Protocol-Oriented Design.
I write in Swift, which supports all of the above. It’s been a while, since I’ve worked with C++, but I have heard that the modern version of the language similarly supports multiple paradigms.
For the frontend, if I am writing for UIKit, I use OOP, and fairly standard MVP, as that is exactly what the framework was designed to afford. I haven’t done much SwiftUI, but I suspect that I’ll be using a lot less OOP, when I do.
On the server end, I tend to use good ol’ PHP. It’s not an especially good OOP language, but I find that OOP isn’t as relevant for my backend work. That work tends to look a lot like very old-fashioned Structured Programming, which PHP is good for. As others have pointed out, I’ve found it easy to go down rabbitholes, with backend OOP.
There’s a lot to be said for an approach as an experienced, multidisciplinary professional.
It seems as if programming and software engineering pundits generally start from the premise that all [other] software developers stink, and rules and tools should be used in an effort to force them to write “good code,” as opposed to, for example, hiring smart people, and training and mentoring them to be adaptable, resourceful, disciplined engineers.
That’s nothing new. I have been seeing this for my entire [long] career. The “new hotness” changes, but the attitudes are consistent.
Here’s an old joke, from the 1980s: https://news.ycombinator.com/item?id=29345907
With PHP, I tend to use OOP constructs as namespaces.
My backend work is fairly humble. Most folks here, would laugh at it, but it works really, really well.
"Specialization is for insects."
After decades developing on a lot of diverse platforms in a variety of languages and models, I've decided there ain't much you can't achieve without structs and functions (or their equivalent).
Sure, any tool is most efficient in the hands of a master who knows how to wield it. But in practice all those shiny extras too often become productivity distractions.
Let's say I have a rotation matrix. That means that the squares of all the entries in any one row or column add up to 1. If I have such a structure, and I find that it has rows that no longer add up to 1, well, what function messed it up? I have no idea. And if I have a million lines of code, that gives me a lot of places I have to look.
But with an object, if I made that data private, only a member function can have messed up. I may still have a thousand lines I have to look at, but that's a lot less than a million. For larger code (tens of thousands of lines and larger), I consider access control to be essential, just to limit the size of the code that could possibly be at fault.
Back in the day, I spent a lot of time playing "who messed up the structure?" in structure-and-function code...
Being able to make a field of a record private does have nothing to do with OOP, even in C you can hide a struct's fields.
But it is not a contest of what can be done, but rather of how clearly, succinctly, and performantly it can be done. Modularity is essential to human understanding, but inimical to performance. Succinctness demonstrates economy of expression, where it matters that what people need done is more succinct than things they don't.
OO as a movement was childishly silly. OO as an organizational technique among many has been fruitful.
I enjoy multi-paradigm languages the most.
Sadly he suicided in 2011 with Sodium Pentothal he bought mail order from China. My final Skype message to him was "goodnight, don't drink the pentothal!" and Skype used to show it to me under his username every time I logged in.
He, and his acerbic nature, are sorely missed.