Jiří Zajpt home

The architecture of Ember.js apps - statecharts

For last 5 months, I've been working with Ember.js (previously called SproutCore 2.0) on building an app for one of our clients. And since that time, we've estabilished what I think is a pretty solid and clean application architecture and I would like to it describe here.

Please take a note that this is not intended to be introductory post to Ember.js, nor the guide to statecharts or MVC. It's intended for people who have some experience with the framework and who know the Ember object model, bindings, and other stuff. If you do not, please take a look at Ember homepage first.

The MVC confusion

The core pattern of our architecture is of course the MVC (Model-View-Controller.) If you've worked with Rails and its MVC, and you think you know MVC, you should be prepared for slight paradigm shift, becausethe Rails in fact uses Model 2 architecture, not "proper" MVC. What is "proper" MVC architecture anyway?

As you may or may not know, the MVC architecture comes from the world of GUI applications, from long before web was even born. In the world of GUI apps, there is no server nor client, just the app itself. For example, when a user clicks a button, the app handles that click in the view layer (often in the form of a click handler or callback.) The view layer then calls the appropriate controller, which in turn can do some operations on models, and then relays the changes back to the view layer. Take note here that it is not uncommon for more significant views to have their own corresponding controller, whereas, for example in Rails, you're stuck with a fact that one page essentially is tied to one controller.

The Rails MVC (Model 2) architecture call chain starts right in the controller layer which is invoked by an HTTP request coming from an HTML page request or an API request (in fact: the router is invoked first, and its job is to invoke the controller.) After, there may be operations involving models. Lastly, the controller renders the template to the client. Rails leverages controllers, models and views as the GUI applications, but the difference is in their interaction and roles.

I hope I've described the difference between the Model2 and "proper" MVC. If not, please feel free to look at the referenced resources at the and of this post pointing to information from people that know a whole lot more about MVC than I do.

Statecharts

When working on an application with many possible screens (views, pages, whatever you call them) and events, you'll find a view-controller interaction (that is: the view calling methods on controllers) pretty hard to maintain. You'll end up with controllers with a many non-related methods called from different views and it all just becomes a big ball of mud. Or at least that's what happend to us at certain point. Surely there must be a better way.

When you think about it, at each precise moment an aplication can only handle a subset of all possible events depending on which screen it is currently showing. So far, the MVC architecture has no mechanism to cope with this or to take it into account. Yes, you can create controllers based on the application state so that one controller equals one application state, but that is not the best solution nor intended by MVC itself.

If are reading carefully, you've noticed that I've just mentioned something called "application state." Applications usually do more than one thing and depending on what the application is currently doing we can then derive its state. For example, a web application usually downloads data via AJAX, processes it, displays it, and then waits for events from the user. In terms of state, we can, for example, have the initial state called "downloadingData" which will take care of firing up AJAX calls and the rest of the chain up until the point when the application is ready to receive events from the user. Receiving events then is totally diferrent state, which we might call simply "ready."

A tool that helps us model an application into states is called the FSM (Finite State Machine, not the Flying Spaghetti Monster.) I think many developers have encoutered this concept, for it can be pretty useful when modeling some of the domain problems. But in our case, we want to use FSM for maintaining states of the application itself, not just a domain model. A package for Ember.js that implements the exact logic needed is called sproutcore-statechart.

Mashing the MVC and statecharts together

You may now ask how does it all come toghether? I know MVC: there are controllers, views and models... but where to put states? And what are they exactly supposed to do? I hope the following diagram can clear things up a bit.

MVC with statechart

User interaction is still handled in views (Ember.View objects) which can have their values bound through the controllers to models. Whenever the view wants to fire some action (for example, a button telling the app to save something,) the view then does not call the controller itself but just fires a named event (for example "save") to the statechart. Because the statechart knows what current application state is, it can decide based on its current state what to do. That can mean going into a completely different state or just firing an action on an appropriate controller.

For more clarity, let's look at an example. Suppose we're showing a page with book information. The user can either purchase the book, add a comment, or go back back to the list of books. Purchasing a book and going back are the responsibilities of other states, so when any of those events happen, we just go to the appropriate state. However, when adding a comment, we want to stay in the same state and just save the comment, so we just tell the controller to add a comment. Of course, the controller knows what comment it should save because the values from comment fields are properly bound to it. A state object's code looks like this:

App.ShowingBookDetailState = Em.State.extend({
  addComment: function() {
    App.bookCommentsControllers.addComment();
  },
  purchase: function() {
    this.gotoState('purchasingBook');
  },
  cancel: function() {
    this.gotoState('showingListOfBooks');
  }
});

If you look closely at the diagram you'll notice that events are also fired the other way, from controller to statechart. This is what I consider a bonus feature of a statechart-capable application. If we continue the example with application initialization and its "downloadingData" state, how does the statechart know that an AJAX call has finished and that it can proceed to a "ready" state? Of course, we suppose that making the AJAX call is the responsibility of another layer (in our architecture: the controller.) We could either pass a callback function or we could just wait for a "dataDownloadingFinished" event and then go to the "ready" state.

Consider the following example using a callback function in the controller:

App.DownloadingDataState = Em.State.extend({
  enterState: function() {
    App.booksController.populate(function() {
      this.gotoState('ready');
    });
  }
});

... versus one that does not:

App.DownloadingDataState = Em.State.extend({
  enterState: function() {
    App.booksController.populate();
  },
  populateDidFinish: function() {
    this.gotoState('ready');
  }
});

In the second version, the populate function of the controller must know about the statechart and invoke "populateDidFinish" when finished.

I personally find the statechart version cleaner, but of course it is a matter of personal taste and explicit versus implicit intent.

Test everything, I mean everything!

An important part of our application's architecture are tests. We unit-test the views, statechart states, controllers, and models. A framework we're using for that is Jasmine.

We do integration testing for our whole application using Cucumber, Capybara and Selenium - which work rather nicely together!

Summary

If there's only one concept you should take away from this article, it's "statechart" (well, not just take away, but use if you can.) I feel I really cannot stress enough how usable they can be.

Anyway, this post ended up a bit longer than I intended and I haven't covered everything so I've decided to wrap this up and write about the rest later (for example interaction with a server - loading and updating data.)

Sources

Fork me on GitHub