Hacker News new | comments | ask | show | jobs | submit login
Deprecating the Observer Pattern (2010) [pdf] (epfl.ch)
77 points by tosh 4 months ago | hide | past | web | favorite | 26 comments



In my experience the Reactive paradigm rarely serves to solve any of the problems it sets out to, and the root of the problem is here:

> 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.


I was pretty enamored of the reactive paradigm until I inherited my first big RX project. Whoof. The amount of stuff I needed to know in order to maintain it was just intense.

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.


A failure mode that has become apparent to me as a downside of having too much team stability: it is very easy to design a system that can only be reasoned about once it has been memorized. You need new people to serve as a mirror to really understand the health of the system.

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.


> 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.


Simple event systems often aren't too bad. There can be problems with order of receipt (what order do events fired from events get observed?). and side effects, and it can be difficult to teach junior devs how to dig themselves out of a problem.

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.


When you start it makes all your code look so declarative and lean but eventually you end up in the same place where events are being sent from all over the place because those are the requirements. Add in fun C++ style stacktraces and it becomes really hard to figure out where issues are originating.


> 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...

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.


>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.


> software complexity ... solved ... streams of events.

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.


That's the problem with dogmatism. Functional programming is great, and there are probably a lot of codebases which could be improved by being a bit more functional, but when you take it as a given that "more (X) == better code" and make design decisions based on that rather than the problem you are trying to solve, sooner or later you will start doing things which don't make sense.


Dogmatism, maybe, but I think maybe it's more symptomatic of just having too much book knowledge and not enough hands-on experience.

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


People keep looking for silver bullets... or thinking they've found one.


> Of course FP doesn't really handle interactive programs well at all, because it is timeless/stateless

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?


If the whole point of this article is to illustrate a new way that reduces cognitive load to the programmer they have utterly failed.

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.


They're not easily understood though. The upfront knowledge cost is high and they can be difficult to debug with the zoo of things from libraries like rxjs


RX looks good on paper, but boy, is it a nightmare in any non-trivial project.

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?


I think it's a nightmare for trivial projects too: it's like building a star destroyer for a todo app, because, you know, one day that todo app may end up being the death star. Who knows. Gotta be ready for that.

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.


I’m in general agreement that Rx is, in practice, not an improvement over most other approaches.

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...


Not sure about the cancellation token bit. Last time I’ve used Rx my experience was that you could never know whether disposing a subscription would cancel the underlying process or not. Many times I wanted the opposite behaviour to what the original author had implemented. Not to mention cases where I wanted to keep a process alive as long as there’s at least one subscriber, but cancel it when there’s none left...

Maybe things have changed since then (2016-2017) with regards to cancellation; but I’m off the Rx train for a while.


Something I find mildly amusing; To implement a RX framework in a language like Swift, one should heavily use observing.


http://reactivex.io/ "The Observer pattern done right" :)


In my opinion, most OOP languages are not powerful to implement "events" right. The work-arounds often mix up conceptual associations with implementation. For example, if I want to associate an "onClick" event snippet with a given button, how that snippet fits into the UI implementation guts shouldn't normally be the app-coder's concern. Most OOP languages don't have enough power to wire up the conceptual association independent of the implementation associations, and the work-arounds are ugly compromises. As an app coder, I want something like this:

  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.


IMO, most commonplace programs can make do with a non-blocking list of last X events. Producers add to the end of the list. Consumers have references to beginning of the list, which are advanced when events are processed. When all references are above an entry, it gets unlinked. You can put abstractions on top of this, but most simple programs wouldn't need to.



Author: Here is a simple example of how to draw a path based on mouse events - you are probably familiar with the observer pattern, making the example easy to understand. Now read the following 17 pages to see why it's wrong.


What is the differences between this Scala.React and ReactiveX? ReactiveX born in 2010 too, and is mentioned briefly in Related Works.




Guidelines | FAQ | Support | API | Security | Lists | Bookmarklet | Legal | Apply to YC | Contact

Search: