Object-Oriented Programming and Essential State
theartofmachinery.com
theartofmachinery.com
We do. It's called Redux. The state is a tree, and each child is immutable and calculated via a pure function of the state and action (called a reducer). A message (called an action) propagates down every branch, and most ancestors remain unchanged, as they don't care about that particular action. So you have an explicit ledger of how state has changed, and figuring out why is trivial. State as an immutable result of pure functions gives you features for "free", e.g. time-traveling through your state. Debugging is a breeze. It's an absolute godsend.
(I realize the video is from 2016 and his opinions might've changed, but I think the speel is still valuable here.)
What a bunch of baloney. My products ofetn live in this exact platonic world: class instances running in multiple threads send and receive asynchronous messages through internal pub/sub bus without knowing much about each other at all. All is working just fine, controlling devices, doing data processing, GPU accelerated display and whatnot.
As an aside, Incidental vs Inherent Complexity is more correct than "accidental" complexity.
("Accidents" happen by chance and if it keeps happening to you -- keep hitting that thumb with the hammer? -- then possibly try 'habitual' and don't blame the hammer.)
But is it OOP that is encouraged or made easy by standard OOP languages? Sounds more like Erlang to me.
It's really hard to have these conversations until everyone acknowledge and understand which OOP is being talked about a given moment.
Perhaps it has implicit critics, as so few people use it.
Basically anything that uses Redux principles.[2] It's how I structure all of my new projects when possible.
[1] https://github.com/reduxjs/redux/tree/master/examples/todomv...
I would have thought that messages are ALWAYS stateless. It is the objects which have a state, not the messages.
"Here is a bad implementation of a calculator, it is bad because of A, B and C. Here is a much better implementation of the same calculator. It's better because of X, Y and Z".
There are no hard rules when it comes to programming and I am very suspicious of any person claiming they are in possession of ultimate truth.
Take the advice as it is and try to figure out if there is something that may improve your process or if it can trigger thoughts that may make you more knowledgeable.
In my opinion this is an implementation detail. A service exposes an interface for CRUD-type operations. The implementation could be any kind of datasource, whether it be a database, RESTful API, filesystem, or mocked data in-memory. Did the author imply that the consumer should be choosing the data source? What about caching? That might be an implementation detail as well -- is it a remote managed cache, a file-persisted cache, or in-memory? what about expiry/invalidation?
This might introduce additional accidental complexity but in my experience building an OOP system correctly has its benefits. An honest question: can FP solve this in a cleaner fashion, with less accidental complexity?
I think the answer is yes, here's my earlier comment which I think is relevant: https://news.ycombinator.com/item?id=18661931
It gets even better, in test the implementation partitions over checked out sessions so that acceptance tests can be concurrent with each test having its own view of the universe in spite of having a single state agent.
While in theory composability and encapsulation are orthogonal concerns when using OOP, in practice (for at least Java and C#) I find that there's often tension between the two.
Once you also lay out perf as a requirement in your abstractions it can really help mitigate these types of problems.
At a high level:
1. Brute force with automated tests. Great if you have known datasets and platforms.
2. Work from most constrained hardware first. Easy to say, hard to do. Back in the X360/PS3 days almost everyone screwed this up and developed for X360 first.
3. If you need to do N of the same things fast, use an contiguous array. If you want to enforce that make the array part of your API. CPU prefetchers are amazing and love predictable memory patterns.
4. Rust is one of the few languages that bakes semantics into the language that line up well with modern architectures. Specifically Rust can automatically apply restrict semantics. It also forces you to think about ownership upfront in a way that tends to be performance friendly.
A reduction in total complexity is another, completely unrelated thing. I honestly do not see accidental complexity on your description. Any system will have to deal with all that stuff, OOP and FP will just do it in different parts of the code.
In my experience as a C# developer, this is not an ideal approach. I assume non-OOP developers have a perspective that this is how we actually go about things. In reality, my strategy is generally to declare POCOs - Plain Old CLR Objects - which effectively just serve to model the business domain as collections of basic properties (basic data types, collections, enums, other model types) without any methods, constructors, attributes, etc.
If you look in the Models folder in any project I currently work on, you will not find a single method declaration in any of the class files. All of the functionality is broken out into crosscutting abstractions such as a Logic or Rules namespace. The general idea is this: Why should I build a model specifically for this one context of usage and have the properties tightly-coupled to their mutators, when I can completely decouple these things and have the models operate with any arbitrary state mutators? In my experience, the modeling of all possible business facts can be done completely independently of how those facts should mutate over time. Another thing to consider is that not all business facts are relevant at all times, but that doesn't mean you can't catalog them all within the same logical business model (e.g. a 'Customer.cs' POCO). Null is a very powerful tool if used responsibly.
Taking this a step further, you can have a business model project (class library/Nuget) that is shared across multiple projects within an organization. Since you have included no specific implementation in the model classes pertaining to how their properties are read/written, you can use these unencumbered in any situation. Mix in a little bit of JSON serialization and you get a really quick and easy way to distribute a common contract and interoperate between various business systems. These projects can also effectively serve as your principal documentation of the business domain model.
Another one that bothers me are these transformation static methods which take type A and return type B based on nothing but the state of type A. The languages were talking about, C#/Java already provide a facility for this called a constructor.
If the development approach is going to completely remove operational methods from data types then I'd think long and hard about using a language which supports this instead of a language, like Clojure, which does not.
Only if it's involved in preserving some object invariants, or accessing parts of its state that are abstracted away in the public interface. Otherwise, you're breaking encapsulation by putting some logic in the object that doesn't belong there, and making it hard to change the implementation later.
> The languages were talking about, C#/Java already provide a facility for this called a constructor.
There are sensible reasons to avoid using constructors for this, at least in the general case.
I've been pondering this thread all day, and I think this is the crux of the issue. Ideally, classes would only encapsulate state and you'd use namespaces/modules to encapsulate functionality. But most Java/C# OOP examples use classes to encapsulate both state and functionality, which gets you stuck in the morass that the article discusses.
C# helps you to some degree with extension methods so you get the same instance.Method() syntax.
Because otherwise, your domain model is not able to express which states are valid. A core idea behind encapsulation is that by controlling state mutations, it is possible to enforce invariants, thereby making invalid states impossible to construct. This idea is not unique to object orientation; for instance, in the static functional programming community, this is known as "making illegal state unrepresentable" [1].
By creating a "dumb" domain model and allowing business rule modules to do arbitrary mutations, each model is required to know about and enforce the domain model’s invariants itself, which dramatically increases the amount of code that potentially can break these invariants, and thus needs to be debugged if something goes wrong.
> Taking this a step further, you can have a business model project (class library/Nuget) that is shared across multiple projects within an organization.
This can work in some situations, but there are good reasons not to do this. Different teams may have differing and sometimes conflicting meanings for some of the terms of the domain (especially for generic concepts such as "user") as well as different data needs depending on the use case. This is why for instance Eric Evans’ "Domain-Driven Design" [2] argues for limiting domain model unification to "bounded contexts" [3] of which there may be multiple in the organization, and have explicit translation ("anti-corruption“) layers between the boundaries of these contexts.
[1]: https://fsharpforfunandprofit.com/posts/designing-with-types...
[2]: https://www.goodreads.com/book/show/179133.Domain_Driven_Des...
Objects should only contain those methods which are required for the invariants of that object. A course registration function is a workflow that involves two types, but does not control their invariants, so it belongs on neither. Most business logic falls into this bucket, and that bucket really works best as a functional paradigm where one can freely recombine the logic to create new workflows.
(The worst answer I've seen is that both end up with a register method, with one just reversing the parameters. So Student.register(Course) just calls Course.register(Student).)
The point I’m trying to make is that the problem is not inherit in OOP. The problem is that domain modeling is hard. If a developer cannot synthesize the above “solution”, I’m not sure a different paradigm is going to help.
In real life, the aptly named "student registrar" is responsible for performing the task. But they're not an entity within this system... Rather they are the context in which this system is running. So a non-bound function actually makes perfect sense... It's bound to the top-level registrar context of the system.
In BPMN, the registrar would be one swimlane, and the registration the process, with additional swimlanes for other actors like the student. The registrar still owns the process, but BPMN allows for other actors to also play parts within the system.
A relational view would introduce a Registration which would join a Course and Students. The workflow would create a Registration, which defacto means that an individual Registration cannot be responsible for executing this.
There is nothing I can see here that calls for FP. You are still mutating the state of the world.
Just use a free standing function. That's like a method on "The world", which is the object where we should put most functions. Programming is easy. (Not: all the technical decisions behind it)
Of course, we can come up with a scheme that requires two validation functions that have more distinctive features than just some number's values. And in this case that might be implemented by parameterizing the function parameter. But, it could also be implemented with a switch for example, and often this approach is more maintainable. And then, you can also have pointers to functions in conventional programming languages. In general, the need for functional approaches is vastly overblown.
I'm finding little too your arguments other than what seems to be a pre-existing bias towards functional approaches being "overblown", along with various alternatives which of course exist, because every programming paradigm is capable of implementing arbitrary logic.
Some abstractions will help you do certain things. If you find yourself constantly duplicating and tweaking the same axiomatic logic to do different things, functional approaches are generally a more helpful abstraction than others.
I'm not positive that functional approaches can bridge a similarly distinctive gap. But I know that it's easy to get tempted down rabbitholes where we constantly restructure programs without measurable benefit, or make abstractions that we regret later on.
Instead, it rather seems to me like mainstream imperative languages adopt a few simple features where they make sense (or not). For example, sometimes it's nice and elegant to look up items using a predicate function, although usually I'll actually prefer the explicit for loop or such (but yeah, YMMV).
One more problem I see is as follows. It seems to me that assembly->procedural (compiled) languages is a step that improves encoding efficiency in a very "local" way. It's still pretty easy to contain and control these abstractions and make different decisions in other places, and still have the places interact easily enough. With more advanced systems, I'm not so sure. There are so many general, far reaching assumptions how computation should be done (language runtime, code generation, etc) that go beyond the mere constraints of the computer's architecture. I think these lead to a lot of isolation that is not beneficial.
> If you find yourself constantly duplicating and tweaking the same axiomatic logic to do different things,
Point is, I really don't. (I'm not sure what "the same axiomatic logic" is, but I don't find myself duplicating a lot of things). Actually, it often seems to me things are much easier and less redundant if we can just focus on the data and not get a type system or similar in the way. Of course there might be problem domains where things look a little different than I've experienced.
And a discussion about composing functions is a discussion about higher-order functions. Why is that not FP?
I also think OOP and FP is a false dichotomy as I use objects functionally every day. And it rocks.
Transport is a relationship between passenger, viechle and state. Some passenger transports with a car, others by foot. It's the Transport's responsibility to update the states of passenger and viechle.
Encapsulation on entities sometimes is not enough to make a good OOD.
Hmm...if your model of OO is one where it doesn't make sense for objects to hold references to each other, then your model of OO is clearly mistaken.
And of course the "origin story" in the Brian Will post is so completely wrong that it doesn't even work when taken somewhat tongue-in-cheek.
Not that there isn't anything to critique in OO, I personally do think it is fatally flawed, but it is the best flawed approach we currently have. However, I think I'd rather read critiques of OO by authors that have actually understood OO.
This is why I love Lisp’s dynamic variables. It’s the best of both worlds.