My Evolving Angular 2 Mental Model: Promises And RxJS Observables
When I first started looking into RxJS, I was excited by the fact that I could unsubscribe from a pending Observable stream. This meant that I could easily cancel pending RxJS streams in my ngOnDestroy() life-cycle method in an Angular 2 component. In the past, with Promises, since they can't be canceled, I'd have to jump through all sorts of hoops to protect against context-sensitive, asynchronous results.
But, the more I experimented with RxJS streams, the more I realized how complex they truly were. That complexity allows for great power and flexibility, no doubt; but, unless you understand the implications of that complexity, I think you can run into problems. For example, I believe that my use of RxJS streams was actually creating a leaky abstraction in my service layer. And, when I was exploring that concept, I realized that I could accidentally allow for partial-stream execution in my business logic.
CAUTION: I should warn you that Ben Lesh - lead engineer of the RxJS team - has expressed that my understanding of RxJS is deeply flawed. And, that my opinions on the matter are misinforming the public. So, please take this all with an understanding that I am in no way an authority on RxJS Observables. And, that the leading authority on RxJS has found my understanding to be lacking.
I am not pushing back against RxJS streams. I think they are powerful. But, when I started using PouchDB in Angular 2 and re-experienced the relative simplicity and power of Promises, I realized that something was wrong with my mental model. I was trying to use an all-or-nothing approach with RxJS. I was trying to "RxJS all the things!" But, the more I sat back and reflected on this mindset, the more I realized that I didn't have a good understanding of my application as a whole.
I'm still noodling on the concept, but here's a visual representation of my thoughts so far:
In this graphic, orange represents Promises and pink represents RxJS streams. Notice that I have included both asynchronous approaches in my mental model. I am not abandoning RxJS; but, I am starting to curate its usage.
In this architecture, I am creating a strong separation between the "Web Application" and the underlying "Application Core," even though they are both client-side code artifacts. The App Core has two primary concerns - commands, that change the state of the application; and queries, that read the state of the application.
NOTE: Clearly, my mental model is influenced by the Command and Query Responsibility Segregation (CQRS) pattern; but, I wouldn't say that I have a strong understanding of that pattern. Let's be honest, most of my code is just procedures that I wrap in object methods.
Going forward, Commands are never streams. They are never interactions that can return more than once. And, they are never interactions that I can cancel once they've been initiated. They go into the app core, they change the state (or throw errors), and they resolve. Commands return Promises. And, if I want to tie these commands into a stream of events, doing so will have to be in done in the "web application" layer using something like the .defer() or .fromPromise() RxJS Operators.
Queries, on the hand, are a lot more relaxed. I'll probably start to use Promises more for queries as well; but, I can definitely see the Queries layer exposing streams for specialized requests, like the canonical "type ahead" example that needs to cancel - ie, switchMap() - the underlying AJAX request after each key-stroke.
The overall mental shift here, for me, is that a stream - that, by definition, only ever returns one event - is not a stream; it's a Promise. But, it can certainly be converted to a stream, in the web application layer, if it needs to be integrated with stream-based workflows. In fact, many RxJS operations already support Promises as parameters; so, this kind of integration is not as clumsy as it might sound.
I can see that RxJS streams are really powerful (even though my understanding of them is insufficient). But, I think that they make the most sense in the "web application" layer of my Angular 2 application. This is the layer that deals with user interactions like mouse-movements, key-presses, and routing. The web application layer then turns around and interacts with the "application core", which is, in my mind, a state machine that returns Promises of state mutation. This is the current mental model in my ever-evolving understanding of Angular 2 development. But, who's to say it won't be different tomorrow.
I spent my whole 2016 developing an Angular 2 app, and most of my time was used to truly understand and think in terms of RxJS.
I watched a lot of talks, i followed Ben Lesh, André Staltz and Matthew Podwysowski like a groupie and i loved it.
I found it intriguing but very hard to grasp and i wonder if a "revolution" like this one can afford to consume that kind of time and effort.
The more i learn about RxJS the more i realize there will be wrong, and leaky implementations out there.
I also think this overhead can became a double-edge sword to Angular 2+ adoption.
Very interesting to hear your real-world feedback. I, myself, have only done R&D with my Angular 2, so I don't have that true rubber-meets-the-road experience when it comes to applying all of these thoughts. That said, this yeas - 2017 - I hope to build and deploy my first Angular 2 application.
For what it's worth, I think even the RxJS team has backed-off a little bit on how broadly they think Observable stuff should be applied. I think someone was saying that they used to evangelize:
> RxJS all the things!
... and now they have dialed it back to:
> RxJS some of the things!
... which I think makes more sense. For me, it's just picking the right tool for the job. And, the beauty is that Promises and RxJS actually play quite nicely together so it's not like you paint yourself into a corner - there are always ways to convert and maps and get the best of both worlds.