Skip to main content
Ben Nadel at Scotch On The Rock (SOTR) 2010 (London) with: James Padolsey and Ray Camden
Ben Nadel at Scotch On The Rock (SOTR) 2010 (London) with: James Padolsey Ray Camden

Creating Leaky Abstractions With RxJS In Angular 2.1.1

By
Published in Comments (6)

UPDATE: After getting push-back on the concept of this post, I tried to come up with a more concrete demo that explains what I mean by a "leaky abstraction" - that the return value of the method was used in a way that was not intended by the author of the method. The abstraction is leaky because the return type enables the consumer to circumvent logic in the origin method.

In Angular 2, there has been a shift in the handling of asynchronous actions - away from Promises and towards RxJS (Reactive Extensions for JavaScript). One of the big selling points of RxJS is that - unlike Promises - streams can be canceled. But, the more I think about RxJS and streams, the more I come to the conclusion that I've been using RxJS to create "leaky abstractions" in my Angular 2 service layer.

Run this demo in my JavaScript Demos project on GitHub.

In application development, a "leaky abstraction" is an abstraction that allows underlying implementation details to leak out into the calling context. With Angular 2 services that use RxJS streams, this is exactly the kind of abstraction that I've unwittingly been creating.

Consider an Angular 2 service that returns data as a stream. Where did this data come from? Perhaps it was pulled out of memory. Perhaps it was pulled out of localStorage or some other device storage. Perhaps it was retrieved from the network over HTTP or some WebSocket protocol. The point is, it doesn't really matter; as long as the returned data adheres to a particular interface, the calling context shouldn't really care where the data came from.

The source of the data is an implementation detail.

And, the mechanics of the underlying stream should be an encapsulated, implementation detail as well.

But, what I've been doing, basically, is returning the Http-initiated streams directly from the service layer. This is leaky. And, you can tell that it's leaky because I need to subscribe() to all service-layer method calls, even ones that could otherwise be "fire and forget" calls. The fact that an external context has to subscribe() to the response in order for an encapsulated mechanism to execute fully is evidence that the underlying implementation has leaked out into the calling context.

If I dig down into my understanding of RxJS - which is admittedly very incomplete - I believe that my misstep was conflating the concept of "unsubscribe" with the concept of "cancel". When I have a Controller that unsubscribes from a Stream, what I really want is for the Controller to stop caring about the response; what I don't want - and I shouldn't want in most cases - is the underlying stream to be canceled. That's a totally different and unrelated intent.

However, when you pass an Http stream out of a Service API, that's exactly what is being conflated: unsubscribe and cancel.

To fix this leaky abstraction and to hedge against such conflation, we can disconnect any underlying Http stream from the stream that is returned to the calling context. In this way, the underlying implementation doesn't depend on the calling context to "initiate the stream" with a .subscribe() call; and, the calling context can safely unsubscribe() from the stream without affecting the underlying implementation.

I'm not so well versed in RxJS; so, the most straightforward way for me to do this was to create an intermediary Promise. By converting the underlying stream into a Promise, I always initiate the underlying stream; then, by converting the Promise back into a Stream, I create a safe way for the calling context to unsubscribe without causing side-effects.

To see this in action, I've created a simple FriendService that fetches a JSON (JavaScript Object Notation) file from the server. The FriendService uses the toPromise() operator and the fromPromise() observable to patch the leaky behavior, creating a point of indirection between the internal and external streams.

// Import the core angular services.
import { Http } from "@angular/http";
import { Injectable } from "@angular/core";
import { Observable } from "rxjs/Observable";
import { Response } from "@angular/http";

// Load modules for side-effects.
import "rxjs/add/observable/fromPromise";
import "rxjs/add/observable/of";
import "rxjs/add/operator/map";
import "rxjs/add/operator/toPromise";


export interface IFriend {
	id: number;
	name: string;
}


@Injectable()
export class FriendService {

	private http: Http;


	// I initialize the service.
	constructor( http: Http ) {

		this.http = http;

	}


	// ---
	// PUBLIC METHODS.
	// ---


	// I return an observable collection of Friends.
	public getFriends() : Observable<IFriend[]> {

		var stream = this.http
			.get( "./app/friend.service.json" ) // Go to the server for the data.
			.map(
				( response: Response ) : IFriend[] => {

					return( response.json() );

				}
			)
		;

		// Just testing with no-latency streams.
		// --
		// var stream = Observable.of( [ { id: 1, name: "Tina" } ] );

		// While the getFriends() method returns a Stream, it doesn't necessarily mean
		// that said stream should be capable of terminating the underlying HTTP
		// request. Doing so could be considered a "leaky abstraction" as it is allowing
		// the implementation details of the Service to leak out into the calling
		// context. By creating an intermediary Promise, it allows the HTTP request to
		// be initiated regardless of what the calling context does with the stream.
		return( Observable.fromPromise( stream.toPromise() ) );

	}

The .toPromise() call acts as a subscriber to the Http stream. This allows the Http request to be sent across the wire, regardless of what the calling context does. The .fromPromise() call then "turns" the Promise back into a Stream so that the calling context can still leverage the power of unsubscribe() on the resultant stream.

To test this, I created a simple root component that fetches and displays the friend data:

// Import the core angular services.
import { Component } from "@angular/core";
import { OnInit } from "@angular/core";

// Import the application components and services.
import { FriendService } from "./friend.service";
import { IFriend } from "./friend.service";

@Component({
	selector: "my-app",
	template:
	`
		<p *ngIf="! friends">
			<em>Loading....</em>
		</p>

		<ul *ngIf="friends">
			<li *ngFor="let friend of friends">
				{{ friend.name }}
			</li>
		</ul>
	`
})
export class AppComponent implements OnInit {

	public friends: IFriend[];

	private friendService: FriendService;


	// I initialize the component.
	constructor( friendService: FriendService ) {

		this.friendService = friendService;
		this.friends = null;

	}


	// ---
	// PUBLIC METHODS.
	// ---


	// I get called once after the component has been instantiated and the inputs have
	// been bound for the first time.
	public ngOnInit() : void {

		// Get the friend stream.
		// --
		// NOTE: This subscription here represents only the local reaction to the stream;
		// the underlying stream (Http request) will still execute no matter what. Heck,
		// you shouldn't even think about the fact that this IS an HTTP request (most of
		// the time).
		var subscription = this.friendService
			.getFriends()
			.subscribe(
				( friends: IFriend[] ) => {

					this.friends = friends;

					console.log( "Friends loaded." );

				}
			)
		;

		// Here's the question you need to ask yourself - should the following line be
		// tied to the underlying implementation of the FriendService?
		// --
		// subscription.unsubscribe();

	}

}

In this version of the code, the root component is subscribing to the response from the FriendService. And, when we run this code, we get the following output:

Creating leaky abstractions with RxJS streams in Angular 2.

As you can see, the request to "friend.service.json" is sent over the wire and the root component responds to it.

Now, let's try this again, but forego the subscription to the FriendService response:

// I get called once after the component has been instantiated and the inputs have
// been bound for the first time.
public ngOnInit() : void {

	// Get the friend stream.
	// --
	// NOTE: This subscription here represents only the local reaction to the stream;
	// the underlying stream (Http request) will still execute no matter what. Heck,
	// you shouldn't even think about the fact that this IS an HTTP request (most of
	// the time).
	var subscription = this.friendService
		.getFriends()
		// .subscribe(
		// ( friends: IFriend[] ) => {
		//
		// this.friends = friends;
		//
		// console.log( "Friends loaded." );
		//
		// }
		// )
	;

	// Here's the question you need to ask yourself - should the following line be
	// tied to the underlying implementation of the FriendService?
	// --
	// subscription.unsubscribe();

}

This time, when we run the code, we get the following output:

Creating leaky abstractions with RxJS streams in Angular 2.

As you can see, even though the root component was disregarding the response, the request for the data still went over the wire. We have decoupled the component from the service and prevented the underlying implementation of the Http stream from leaking out into the calling context.

Want to use code from this post? Check out the license.

Reader Comments

15,848 Comments

@All,

I had a Twitter conversation with Ben Lesh, lead of the RxJS project, and I feel like I should point out that he feels my point-of-view here is incorrect. That my approach here represents anti-patterns for Streams.

That said, I wanted to try an perhaps more clearly articulate my point-of-view:

www.bennadel.com/blog/3185-follow-up-creating-leaky-abstractions-with-rxjs-in-angular-2-1-1.htm

Perhaps Ben Lesh can drop in here some time and expand on his interpretation of my perspective.

15,848 Comments

@All,

Here's a comment that I just left in another post that I think perhaps codifies my mindset nicely. To me, the use of Observables in a service layer is an implementation detail, not a fundamental change in behavior.

From post: www.bennadel.com/blog/3186-using-hot-rxjs-observables-in-your-service-layer-in-angular-2-1-1.htm

----

Perhaps the biggest disconnect is that I simply don't view Observables in a *service layer* as being fundamentally different than anything else. To me, it just seems like an implementation detail. Meaning, I could have an API that uses Callbacks:

service.doSomething( inputs, callback );

... or refactor it use Promises:

service.doSomething( inputs ).then( callback );

... or refactor it use Generators (hope this one is right - I'm a noob there):

yield service.doSomething( inputs );

... or refactor it use Observables:

service.doSomething( inputs ).subscribe( callback );

In any of those cases, I wouldn't expect the *behavior of the service* to fundamentally change - only the way in which I was listening for the response. But, if we're saying that an Observable fundamentally changes the way a service works, then I think we just have two different mental models for what a service does. In my mind, it's just an implementation detail.

15,848 Comments

@All,

I've spent a month and half noodling on all this RxJS stuff; and, as I've continued to evolve my mental model, I am now returning back to Promises for the "core" of my application:

www.bennadel.com/blog/3202-my-evolving-angular-2-mental-model-promises-and-rxjs-observables.htm

I'm thinking about my application architecture as a layered system with the "web app" layer on the outside and the "app core" on the inside. The App core is where all the real "business logic" is and it will be broken up into Commands and Queries. The Commands portion will be Promises. All of it (that I can see). But, the Queries portion will be a mix of Promises and RxJS, when it makes sense.

And, in the web-app layer, with the Controllers, I'll use RxJS when it facilitates user-interaction workflows. Of course, Promises dovetail quite easily with RxJS streams, so I don't see any real complications here.

Anyway, still noodling, still evolving.

I believe in love. I believe in compassion. I believe in human rights. I believe that we can afford to give more of these gifts to the world around us because it costs us nothing to be decent and kind and understanding. And, I want you to know that when you land on this site, you are accepted for who you are, no matter how you identify, what truths you live, or whatever kind of goofy shit makes you feel alive! Rock on with your bad self!
Ben Nadel