Deprecating the Observer Pattern (2010) [pdf]
infoscience.epfl.ch
infoscience.epfl.ch
> The first step to simplify event logic in an application is to come up with a general event interface so that all event handling code can work with a uniform interface.
RX proponents seem to make the leap that the problems of software complexity can be solved by wrapping your logic into a highly general abstraction layer which homogenizes everything your code does into streams of events. I'm not entirely sure what this assumption is based on.
To look at how this assumption breaks down, just look at the ReactiveX API: what's billed as a general purpose tool consists of a menagerie of observable subtypes and operators built to patch the holes in real-world problem complexity left by the "general case" solution described by this article. Or look at the serpintine call stack in any real-world RX project of any non-trivial complexity.
The more I code, the more convinced I am that the best practice is to write software at the lowest level of abstraction possible, as close to your problem domain as possible. Paradigms like RX which seek to magically reduce complexity through generic abstraction at best move that complexity into another layer where it's more obfuscated, and at worst introduce additional complexity.
Nobody else liked it, either, so we slowly, over the course of a few months, replaced the reactive bits with more ad-hoc code. The end result was smaller, easier to read and modify, and more performant.
There’s a term from compilers that some people have borrowed to describe thinking about code: Local Reasoning.
Lots of things can kill local reasoning. Global variables, overuse of delegation, mutation of someone else’s objects, and aggressively event-based systems.
I'd be interested in reading about your observations on how event-based systems kill local reasoning.
Is it simply a bad design that requires the publisher to understand who/what all the subscribers are doing with an event?
I've found that designing the semantics of individual events, and constraints on their interpretation, is the hardest part of event based systems so I could see that becoming an issue.
But when the system is using events as message passing mechanism between modules, things eventually tip over into Bedlam. You have events that disappear, or broken events that you can't track back to the source (where is this shit coming from??).
The worst problem, the sucking wound I've seen happen a number of times, is when an event starts repeating. Somehow you make an event that causes itself to fire. In a loop, forever until too many pile up and the app stops responding.
There is a subset of developers who long for developing "The One" architecture for a given domain. This is the architecture that can accept any change with minimal code changes. The vast majority of these developers tend to be Enterprise devs and are very outspoken about the way software should be developed. I don't believe any of them will ever succeed as once the project becomes unwieldy, they leave for other pastures.
On the contrary, this is the formula to success: Outrun Your Mistakes.
If you make your code look sufficiently complicated and meta the team will praise you as a genius after you are gone, even as they try to untangle problems that you created.
I think I have an idea how this happened. First, it seems to me that there is a large overlap of the extreme RX proponents with extreme FP proponents, who have been rather messianic in touting FP's ability to solve all software problems.
Of course FP doesn't really handle interactive programs well at all, because it is timeless/stateless, simply mapping inputs to outputs. So that's a big hole and a bit of a problem.
Enter (synchronous) dataflow programming (Esterel, Lustre, Lucid), which moves time into streams, and voilà, now we can apply FP to this domain as well and everything will now obviously be rainbows and unicorns.
Obviously.
I'm starting to have this experience where I'll advise a more junior programmer to exercise caution about $THING_FROM_BLOGOSPHERE because, while the code examples from the blog post certainly do make things look nice, I can see where it might develop XYZ problem when you try to use it in production code. Typically I'm ignored, because I'm clearly just old-fashioned, and possibly even starting to go senile. Typically, after 6 months or so, you'll see that developer discovering that they're running int XYZ problem.
This is all cranked up to 11 with functional programming, because what a lot of newcomers to FP don't fully appreciate is, avid functional programmers are people who love to play with abstractions. Emphasis on the word play. My sense is that a lot of the stuff that people are putting out there is a mix of spitballing new ideas and stuff done in the spirit of RFC 1149.
see also: https://www.willamette.edu/~fruehr/haskell/evolution.html
Interesting claim. How is it that people write, for instance, parsers in functional languages if they have no state? What is it that the State monad does?
Observers have staying power because they are easily understood with current common programming language syntax.
To truly replace the observer pattern, we'll need to come up with a simple way to describe a better pattern. I'm unconvinced that RX is the better solution based on this article.
Is this stream hot or cold? Will it fire the request every time I subscribe, or at most once? On which thread is this stream delivered on? UI thread? Let’s be safe and dispatch it to the UI thread! Oh, now my UI flickers because this other stream is producing this event one cycle too early... Let’s add one millisec delay here and hope it fixes the issue (yuck.). How do I cancel this stuff?
But:
> How do I cancel this stuff
Everyone I’ve ever heard describe Reactive pipelines describes built-in, standardized cancel behavior as the raison d'être for Rx!
It’s super common to encounter a one off someone thought would never get called more than once in a while, which actually piles up a huge serial queue (or worse, creates dozens of threads while concurrently contending for a resource that’s serialized in a less than perfectly transparent way).
So in those cases, knowing all your interfaces explicitly include a cancellation token is pretty appealing.
Of course, in practice, lot’s of people don’t bother to wire together the cancellation bits, so you’re left thinking you can cancel things but it doesn’t actually do anything. So maybe that’s what you meant...
Maybe things have changed since then (2016-2017) with regards to cancellation; but I’m off the Rx train for a while.
Another thing: debugging RX. God, it hurts. Like Alice in wonderland.
I worked with an rx guru on my last project and while the code was beautiful, huge and complex, showing a load indicator or handling simple ajax errors, doing persistent navigation states (and hoping that the back button would not break the application) were just omitted altogether. It was an engineering marvel, and a usability trainwreck with around a six megabyte initial payload and no lazy loading at all.
define buttonX inherits button {
buttonAttribute1...
buttonAttribute2...
buttonAttributeEtc...
method onClick() {
doSomethingWhenButtonPressed(...);
}
} // end define
I don't want to have to "register" the event with an observer or whatnot; that's low-level grunt work the framework should normally take care of.Doing it "right" would require something akin to relational modelling, but there's no current language/IDE that embeds source code into RDMBS well. It's an area ripe for research. Large OOP frameworks don't seem to scale multi-associations well and I find myself looking at RDBMS.
https://engineering.linkedin.com/distributed-systems/log-wha...