reactive programming demos are impressive. two of my favorites are:
it’s surprising that such succinct code can accomplish such messy tasks with so few abstractions. but when the elegance combines with unfamiliarity, it’s easy to forget that wisdom we’ve learned through hard years on the front lines of app development:
reactive programming should be combined with the design patterns we know from mvc0. while those little demos seem to imply that reactive programming supplants mvc, the two are quite different. reactive programming proposes a small set of primitives for managing state with data flow, whereas mvc separates application concerns.
in a good mvc app the state is in the model, so let’s look at how we can use reactive programming for the “M” and keep that separate from the “V”.
we’ll start with an example of a simon-says game1, implemented in RxJS, but without any separation of concerns.
the game will show the user a sequence of numbers and a set of numbered buttons. the player pushes the buttons in the order of the numbers. they win if they get them all correct, otherwise it’s a loss. they can restart the game at any time. here’s an example:
i know, i’m horrible at making games.
the code can be found in spaghetti/index.js
, but i’ve extracted the relevant part below. it reads top to bottom, so i’ve just explained the code inline.
// this is a stream of click events
var newGameClicks = Rx.Observable.fromEvent($newGameButton, 'click');
// this ends up being a stream of game results, which emits every time a game
// completes, but the road to get there is a bit complex, so it's explained
// below
newGameClicks
// generate an array of numbers on every click
.map(arrayOfRandomNumbers)
// use the array of numbers as the data to render the game and pass it through
.do(renderGameAndHideResult)
.map(function(order) {
// for each array of numbers, return an event stream of button-clicks
return Rx.Observable.fromEvent($game.find('.number-buttons'), 'click')
// on each click, given the previous state, we pass along the new
// `gameState`, which contains 2 peices of information:
//
// - the original order
// - the buttons pressed so far
//
// so after this call we get a stream of game-states which emits every
// time the user clicks a number button
.scan({order: order, pressed: []}, function(state, event) {
var val = +$(event.target).val();
return {
order: state.order,
pressed: state.pressed.concat([val])
};
})
// we have 2 termination conditions:
//
// - wrong button press
// - the player got them all correct
//
// so we make the stream of game-states end on either of the above
// conditions. note this uses a helper function defined in lib/common.js
// that acts the same as RxJS's `takeWhile`, except it includes the last
// item.
.takeWhileInclusive(function(state) {
var prefix = state.order.slice(0, state.pressed.length);
return _.isEqual(prefix, state.pressed);
})
.take(order.length)
// we only want to concern ourselves with the state of things when the
// game ends, so this returns a stream of only one game-state, which
// emits on the last click
.last()
// also end the stream if the user requests a new game. this still
// returns a stream of only one game-state
.takeUntil(newGameClicks);
})
// now we've got a stream of streams of single game-states, analogous to a
// nested array of objects -- `[[{}], [{}], ...]`. we really just want a
// flattened stream of the final game-states, so the call below takes out the
// *inner* streams
.concatAll()
.do(renderResultAndHideGame)
// in RxJS, these event streams aren't active til you call something like
// subscribe or forEach
.subscribe();
true to form this implements the game succinctly using common reactive programming idioms, all built on one abstraction: the event stream (or Observer
in RxJS parlance). it doesn’t have the familiar looking class or view definitions we’re used to seeing in popular mvc frameworks, but it’s got all those side-effect free functions… surely this code that would garner a nod of approval from functional purists, right? this code doesn’t feel right though. it’s mixing our application’s data model (the order of the numbers, the results of the games) with displaying the data.
it’s the jquery spaghetti of functional javascript.
we want to be able to evolve this app without changing the above code, but it’s easy to think of features that would trip us up:
pushState
more generally, there are questions we can ask as a litmus test of whether our UI components are maintainable:
the code above doesn’t meet any of these.
the underlying data model of this application could look something like this:
the new game stream can’t just be hard-coded to come from a single button’s clicks though. we need a level of indirection where any event stream can be connected or disconnected from the new game stream dynamically.
to accomplish this we’ll have to introduce a new abstraction, something RxJS calls a Subject
that both consumes and produces events.
var subject = new Rx.Subject();
subject.subscribe(console.log);
subject.onNext('foo');
// => 'foo'
we need to produce arrays of random numbers though, regardless of what is passed into the onNext
call.
var subject = new Rx.Subject()
subject.map(createArrayOfRandomNumbers)
.subscribe(console.log);
subject.onNext('foo');
// => [1, 3, 1, 0]
but now the problem is map
returns a new observable, and we need a single object that both consumes the onNext
calls and produces the arrays.
var subject = new Rx.Subject();
var observable = subject.map(createArrayOfRandomNumbers);
var newGameStream = Rx.Subject.create(subject, observable);
newGameStream.subscribe(console.log);
newGameStream.onNext();
// => [2, 2, 0, 3]
there’s one more subtlety. the observable
variable will produce a new value for each subscription, not onNext
call. that means different observers would get different arrays of values, defeating the purpose of using a Subject
in the first place.
newGameStream.subscribe(console.log);
newGameStream.subscribe(console.log.bind(console, 'second subscription:'));
newGameStream.onNext();
// => [0, 1, 3, 2]
// => second subscription: [3, 3, 0, 1]
in cases where the behavior is deterministic, this would be preferable, since it insulates us from accidentally sharing data. however in this case we want every subscription to get the same value, so we’ll share the Observable
.
var subject = new Rx.Subject();
var observable = subject.map(createArrayOfRandomNumbers)
.share();
var newGameStream = Rx.Subject.create(subject, observable);
newGameStream.subscribe(console.log);
newGameStream.subscribe(console.log.bind(console, 'second subscription:'));
newGameStream.onNext();
// => [2, 1, 0, 3]
// => second subscription: [2, 1, 0, 3]
a similar approach can be applied to the stream of results, allowing us to decouple the reactive data model from the views.
the repo has the full listing of a reactive-programming-driven model for this game in lib/common.js
, and examples of using it with plain js views and react.
this design enables simple views responsible for nothing beyond converting the data to DOM. the views are modular and testable, and the model doesn’t need to be modified for every new feature. we’re reaping the benefits of reactive programming while maintaining mvc’s separation of concerns.
[0]: there are different flavors of mvc, and this article is mostly concerned with the model and view, so i’m using “mvc” as a generic term.
[1]: the idea for the game comes directly from @lawnsea’s talk, which was kinda the inspiration for the post.
blog comments powered by Disqus