When objects are not enough (2021)
tonysm.comThe objects are just one tool. You usually need a few to do the job well.
The biggest thing I've seen blow up actual OOP projects has been a lack of respect for circular dependencies in the underlying domain. If you have one of those problems where it is ambiguous which type "owns" another type, then the moment you start writing methods in these types you are treading into the dark forest. Often times it is unclear that your problem exhibits circular dependencies until code is already being written and shipped.
My approach to these situations is to start with a relational data model. A SQL schema (and its representative DTOs) can model circular dependencies competently. You can then have additional object models (views) that can be populated by the same relational data store (just a different query). One other advantage with the relational modeling approach is that it is very easy to explain things to the business before you write a single line of code [0]. The purpose of a SQL table can be demonstrated with a sample excel sheet with mock business data.
This path was largely inspired by Out of the Tar Pit [1] and practical experience in fairly wicked domains (semiconductor mfg., banking, etc). I am not sure Functional Relational Programming is the answer for everything, but the "Relational" part certainly seems to be universally applicable.
[0]: https://en.wikiquote.org/wiki/Fred_Brooks#:~:text=Show%20me%....
Why would a type ever own another type? What would that even mean? Like an inner class? Or are you taking about ORMs?
Think about a collection of one related type on another, such as Customer -> Accounts + Account -> Customers. In banking, it is possible for one customer to have many accounts, and for each account to have many customers.
OOP will let you express this kind of problem from a data modeling perspective (List<T> on each type), but from a serialization and dependency perspective you have to pick a "winner". In banking, it is unclear which type should be king.
The relational approach is to use a join table. Model the actual relationship itself and its relevant attributes (role on the account, beneficiary %, etc). This also handles any arbitrary graph of accounts and customers, assuming you are using a modern database engine that supports with recursive & cycle keywords.
ORMs will blow up on this kind of thing without special handling.
> OOP will let you express this kind of problem from a data modeling perspective (List<T> on each type), but from a serialization and dependency perspective you have to pick a "winner". In banking, it is unclear which type should be king.
Serialisation is not necessarily a big deal, just pick one and refer back to it — check out PRINT-CIRCLE, Sharpsign Equal-Sign and Sharpsign Sharpsign in Common Lisp.
For dependencies, I think this only matters with strong type systems which don’t support forward declarations, or which lack null references/empty containers (if the language does support that, just create one object without any references, create the second, then add the second to the first).
This is related: Why relations are better than objects https://www.cell-lang.net/relations.html
> Why would a type ever own another type?
FHIR, an international standard for medical data, is a great example here. The circular types get so gnarly that I've personally managed to infinite-recursion the TypeScript compiler. We even managed to get 45min builds followed by a timeout by sneezing wrong.
https://www.hl7.org/fhir/overview.html
The tldr is that you can have things that belong to things that belong to things that end up belonging to the original thing after a very very long inscrutable chain of pointers. Essentially a graph.
Maybe like how having a handle back to something can be a parent-child relationship or can imply that the child owns the parent, especially if the lifetime is extended by the ownership (e.g. shared pointer).
I want to re-read this and think about it more.
But one thing stuck out: I never liked most of the distillation of actions as clean architecture would advocate for, be they lambdas in class form (DepositAction) or interactors. I feel strongly that they are the right thing in describing business logic, however.
What did click for me is the conceit of a service, which the JVM world embraces. Services are plain old objects that have a method for each action you'd like to model. This is nicer than the aforementioned approaches because there is often common code to different actions. Like actions, services are the place where validation happens, persistence happens, and all of the interesting business logic. IO and orthogonal concerns are injected into them (via constructor), which lets you write tests about the core logic pretty easily.
What you get is the ability to reason about what happens without the incidental complexity of the web. Web handlers then boil down to decoding input, passing it to the service, examining the result of calling an action, and then outputting the appropriate data.
That's all they should've ever been doing. :)
"Services" in the JVM, usually in the sense of Spring or some other service management framework, is just procedural code with a thing object instantiation layer which allows late binding invocations across services, which in complicated code turns out to be enormously useful:
- enable/disable/manage caching layers to procedure... uh... method calls
- play tricks with remote invocations that look like local ones
- enable advanced testing frameworks with mocked parameters and data connections
- enable/disable logging at runtime, and target specific services
- "aspects" to target various patterns of invocation and inject interceptors/decorators/etc to the procedure... uh... method call.
JVM service management OOP is a very different thing than domain/data object modeling OOP. JVM service management OOP is a slam dunk in terms of delivered value. In the referenced Alan Kay bullet points, it is mostly extreme late binding, and kind-of message passing that delivers tremendous value.
Data modeling OOP and GUI framework OOP is the old dog is-a animal, but is-a pet and all that headache. Because Java's behavioral compositional model is basically single inheritance (yeah, there's interfaces if you want to copy-paste or write your own delegates), it is fundamentally limited. That is the OOP that has squarely and properly been questioned over the last 10 years.
> An object has state and operations combined.
My own definition has always been that an object has state and identity.
I have never considered functions/methods to be a requirement for something to be an object.
I think you're right that, in the object-oriented world, a method isn't a necessary condition for most people to consider something an "object".
What the author seems to be saying is that if someone asked you to sell OOP, one way you'd sell it is by mentioning that an object can couple logic and state. That's a distinguishing factor between objects and other data structures.
> one way you'd sell it is by mentioning that an object can couple logic and state
Depends on the OOP paradigm. This is not always the case.
Yeah, I think the best object systems (CLOS and friends) intentionally decouple objects from functionality.
It's not a requirement, but an immutable object has fewer uses than a mutable one.
Functions/methods/actions/operations are just different names for the operations which mutate the state of the object. So, I would argue that they are a necessary attribute of mutable objects.
Object identity effectively implies mutability. Without that implication, two objects of the same structured value being non-equal doesn’t mean anything (and would probably be better classified as a mistake).
Another way to look at it is that property setters (or whatever mechanism is used to directly mutate an object’s sub-data) is not meaningfully different from a method doing the same. You could even call it syntax sugar for the same.
My take is that identity doesn't imply mutability, if you version objects. It could well be that your are looking at an old version of an object, using its unique identity combined with its version (number).
Objects refer to other objects using their (immutable) identity. In turn, resolving identities to objects requires (version) scope which can be in the past or present.
> My take is that identity doesn't imply mutability, if you version objects. It could well be that your are looking at an old version of an object, using its unique identity combined with its version (number).
Are you storing the version as part of the object? If so, they’re no longer equal values regardless of identity. If not, what purpose is there in versioning the same value? Even if there is a purpose, are same-value-different-version objects not otherwise interchangeable unless/until some value change does occur?
Identity doesn't imply 'value' equality, that's the whole point of mutability! Conversely, two objects can have the same 'value' while having different identities. Values and objects are different beasts.
> Identity doesn't imply 'value' equality, that's the whole point of mutability!
That’s exactly the point I started with!
> Conversely, two objects can have the same 'value' while having different identities. Values and objects are different beasts.
Agree completely. My point—my only point—was that identity implies mutability. Without mutability, identity distinct from value doesn’t mean anything.
This point is actually emphasized strongly by the CS giant, Guy Steele himself.
OOP schemes like CLOS, Dylan, S4, and others keep Objects and Actions separate. This is similar to Haskell type classes.
Maybe, but mutability does not require inbuilt operations.
I have often used data-only objects, and passed them to fixed-context functions.
It's a cheap way to get OO behavior, in non-OO languages.
I would still classify that as object oriented behavior.
You have a class/type of objects, and you have a set of functions or operations which are associated with (loosely or tightly) and operate on that type of object.
For example, I would say that file descriptors are a handle to a type of object. They encapsulate the thing that they represent (file, DRM buffer, eventfd, whatever), and some of the functions that operate on them are polymorphic. For example, read, close, ioctl, etc., don't care if the file descriptor is a file, a queue, or whatever.
You use syscalls to interact with file descriptors, but they are just as much object handles.
It definitely is. I was just talking about the definition of "an object."
It separates the functionality from the object. Turns it into an external stimulus to change the state of an object.
I used this pattern, back in the 1990s, to make a C API behave like a C++ API. This was back when every compiler had a different stack convention, and the only one we could rely on, was C.
To be fair, I did have function pointers, in some of the objects, that acted as "poor man's vtables."
I know that the API was still in use, 25 years later.
It's possible that we are talking at cross purposes.
I think you might be arguing that the object itself does not include the set of functions/methods/operations that act on it, whereas I see them as another attribute of the class/type which is an attribute of the object.
Yeah, object orientation isn't just a language feature, it's a pattern for structuring interaction with data.
And as for object orientation being less useful in immutable languages, I see it used plenty.
Erlang and haskell both define dictionary types that return new copies of the dictionary on what would normally be mutating function calls. The dictionary's gory details are hidden behind the object's mask, leaving the user free to ignore if it is a hash into an array of arrays, as many dicts used to be and as allows sharing most of the entries between copies of the dict until resizing, or maybe it's actually a balanced tree or just an a-list.
The outer code doesn't need to know because you create and manipulate the dictionary abstractly through helper functions, be they attached via the language or simply exposed while leaving the implementation opaque.
Object orientation will also be used to hide the implementations of files, sockets, and other abstract resources.
Many interfaces throughout the linux kernel use a plain C form of object orientation, a language which is definitely not object oriented on its own, filling out an 'interface' structure with functions to be used for objects originating from a particular area, allowing the kernel to interact abstractly with filesystems via the function pointers in the struct, for example.
We could say that mutability (or having functions/methods/etc) is part of the object’s identity.
I consider identity to be an object that is exclusively itself, and knows what it is. You can treat it in an opaque manner, and its own identity will dictate how external stimulus works on it.
So weird this is getting down voted. Immutable objects would just be structs with sugar. It's mutability that gives you the power to keep a reference to an object, but have it keep up with the operational lifecucle of your app/process/..
+1 for use of the word "Reification"!
There's another universe of object-adjacent systems that may or may not be connected with conventional OO programming languages. Two examples I'd point to are Microsoft's COM (designed so it is straightforward to write and call COM objects from C) and the "objects" in IBM's OS/400. In both of those cases I think the reification is the important thing, although you can see reification in Java's object headers where objects get a number of attributes necessary for garbage collection, concurrency control, etc.
COM dates back to a time when OO languages were uncommon but the techniques were being used in C.
I worked on a system back then that had “objects” that was based heavily around function pointers.
When we first got hold of the cfront C++ pre processor it did much the same thing but automated all the kludges we had for compile time checking.
So I wouldn’t really class something like COM as object adjacent, it was more “proto” OO
> Smalltalk was one of the first Object-Oriented Programming Languages out there. It's where ideas like inheritance and message-passing came from (or at least where they got popular, from what I understand);
It may be worth pointing out that while Smalltalk is arguably one of the key languages that popularized such ideas and other OOP concepts, these were first introduced by Simula 67.
I enjoyed this post a lot. I happened to blog about the same thing from the FP side of things [1] (well, because Closures are just the poor man's Objects).
Generally, I don't know how to (philosophically) navigate the tensions between Functional / Object Oriented / Imperative / Declarative paradigms, except to remind myself about The Thing That Actually Matters (in my estimation)... to always remember that The State is the frenemy.
For the love of State is the root of all evil: which while some coveted after, they have erred from Lambda the Ultimate, and pierced themselves through with many sorrows. --- Yours Truly.
:)
[1] https://www.evalapply.org/posts/what-makes-functional-progra...
(edit: forgot to link to blog post)
I never liked „OOPs world“ obsession with a receiver. Why the hell receiver of a message gets to dispatch the implementation at very late moment? Why not context? And when there are many receivers? I.e. a composite object of an int and a float receive message „add“ - who decides which implementation to use and why the heck any of them may even know how to add self to another? There were many attempts to solve this and all were horrible (i.e. extension methods). I have been experimenting with these concepts too (i.e. a receiverless message is a throw, which will be picked up by an enclosing effect handler via pattern match), but its all unreadable and unmaintainble mess. Anyone doing the same thing? I would love to exchange thoughts :) extras to read: ian piumarta‘s papers on his OOP system and a paper on Korz programming language
> Why the hell receiver of a message gets to dispatch the implementation at very late moment? Why not context?
Well, then it is the context who is the actual receiver, isn't it? Since in this scenario the object may never even receive the message since the context has already processed it on its own, so calling it a "receiver" would be incorrect.
> a composite object of an int and a float receive message „add“ - who decides which implementation to use
The composite object itself, who else? It can do anything, including doing nothing, or not using any of their implementations, or dividing its int by its float, etc.
Maybe I'm misunderstanding but the whole point of any messaging system is that the sender "doesn't know how to process the message" and it's up to the receivers to process it and coordinate amongst themselves. Otherwise you could just do the processing where the message was sent. For non-OOP examples you have most service architectures.
So from that perspective an int-float object would be built by composition and the containing object would receive and process the message before dispatching to it's component objects as it saw fit to accomplish the task of being an int-float object.
In OOP as I understood it, messages are first-class entities at the same level as "objects". This is strikingly different from modern OOP languages where a message is conflated with "function calls". This is pointed out in the article: "[I]n Smalltalk, messages 'were not real messages but disguised synchronous function calls', and this mistake was also repeated in other languages[...]".
If a message is a first class entity, then an object can technically have only one "function call" -- receive message.
So that an object can simulate an arbitrary set of functions with arbitrary arguments, the implementation of the "receive message" function cannot impose conditions on the format or content of the message. Instead, the message must be encoded in a self-describing format so that the object can interrogate the message and its contents, and only then decide what to do with that message, up to an including ignoring the message entirely.
To make this more concrete, imagine having an JavaScript object that has only one method: receive messages encoded as JSON strings. With JSON strings we can say that the message is self-describing and is easily parsed by the object. Once the JSON string is parsed, the object can then decide what to do based on the content of the message. This is both a late-binding and a dispatching activity.
It should be clear that the version of OOP does not include anything about types. That's because OOP was designed with LISP-like languages in mind, where symbols were processed and strongly-typed objects. It also means that build-/complile-time checking wasn't possible.
I'd say the modern web with JavaScript and HTTP calls is more like the original OOP design than any modern "OOP"-like programming language.
Does send() and method_missing() in Ruby fit that bill? From what I remember from Ruby, and it’s been a while, all method calls are just a message via send() with a symbol and arguments. Normal method calls are just syntactic sugar over this system. With method_missing() you can handle any messages that use a symbol that doesn’t match a method name. You could make the object handle messages in a completely dynamic way.
Yeah I was intentionally not talking about OOP but about messaging because I think the concept quite obviously transcends the programming paradigm and I often use it as you suggest.
That said one way of processing messages is to relate the message name to a method name at runtime and that is quite successful which is where I think the stronger version of that linking comes from in other languages that have static binding. It's still the same messaging concept but the routing is resolved at compile time which has tradeoffs.
then why the receiver would know too? There are more parties in this orgy: message receiver, message sender (method caller), environment (static: like imports), context (dynamic: like stack). Why the heck of all of those the receiver decides?
I'd take all of those as receivers if they are reacting to a message being sent.
To give another example in a game engine I recently worked on we sent messages between game entities and the sending entity could target itself directly and indirectly with no problems and a common pattern to keep components of the entity decoupled was to do just that.
In some OOP languages the environment and everything else are all objects. I think Smalltalk itself works that way. Which reflects this way of thinking.
If you want a "receiverless message", you want a queue surely?
possibly related food for thought: google up DCI?
Why not generic methods like in Common Lisp?
I want to agree with this but it’s a surface level concern. I’m not trying to belittle the author’s point but I do want to emphasize something that isn’t talked about enough.
Reification has two meanings in software development. Author uses one, the more familiar one - it’s discussed in many books on DDD. The other is a bit more vague and unapproachable. It’s the one I want to bring to peoples attention. It can be summed up as: “Accounts, transactions and balances are not enough.”
Jim Weirich hints at this perfectly in his approach to solving the Coffee Maker [0]. I’ll quote the punch line for everyone:
> Some people may be uncomfortable with the divergence of the Analysis and the Design models. Perhaps they expect that the design model will just be a refinement of the analysis. Remember that analysis is an attempt to understand the problem domain. Although we use OO tools to represent our understanding, that understanding has only an indirect influence on the structure of the software solution (in that the software must actually provide a solution). In fact, as we refine our software design, we will find that it moves even farther away from the analysis model. Solutions oriented classes, which do not appear in analysis, will be added to the design. Classes that came from the analysis model may mutate or even disappear entirely.
This is the form of reification that I believe is more closely associated with how the term is used in classical OOD (f.e. Object Oriented Software Engineering: A Use Case Driven Approach), but I suppose people’s experiences may disagree. This is a kind of “right layer of abstraction” that has to be pontificated and not just put down as an object because it’s a process or a “thing” from the outwardly-behavioural view of the situation.
As I mentioned, it’s not talked about enough. Jim is right to call it out: it’s uncomfortable because it’s having to dig deep into the system ontology.
[0] http://www.cs.unibo.it/cianca/wwwpages/ids/esempi/coffee.pdf
What works for me:
There are two types of objects: State objects and Tool objects.
The purpose of a State object is to maintain a state. Think strings, databases, files etc.
The purpose of a Tool object is to operate on State objects. Think loaders, printers, editors, transformers etc.
It’s a simple way to organise OO code. And it works. At very large scale (millions of lines of C++).
It is similar to the functional Data/Function thinking. However it actually works better because State objects can enforce constraints, and Tool objects can cleanly maintain an internal temporary state while operating on State objects.
Objects are just the poor man's actors. If you want to see what encompasses objects and more, look up the actor model.
That's what I thought this article is going to talk about but not even a honorary mention.
Needs "2021" in the headline
Yet another article about problems you'd never have if you wouldn't use object oriented paradigm.
But then you'd have other problems that come with using another paradigm, since there's no silver bullet, and no paradigm that handles all problems better than other paradigms. Probably popular languages tend to be multi-paradigm.
That's not really true. Structured programming completely supplanted the paradigm that predated it. I think it's pretty close to a consensus now that null values are a mistake. Same with manual memory management.
Programming paradigms meaning imperative, functional, logical, OOP, stack-based, array-based, that sort of thing.
Widely used languages like C++, Javascript, Python allow for a mix of those approaches. If one programming paradigm was best, we'd expect languages like Haskell, Prolog or APL to be popular instead.
The contemporary "multi-paradigm" style is influenced by the many paradigms which preceded it, but for all the elements it borrows, there are elements it leaves behind, too. Implementation inheritance is often left out of newer languages (see Rust, Go). I don't think it makes sense to view new langauges and styles as the sum of all preceding paradigms. It's an evolutionary process, rather than accretive. We keep the good bits and discard the rest.
I read the article and I'm not even clear what the problem is.