What do you get when you cross Ember.js with the Single Responsibility Principle? I recently decided to find out by refactoring the architecture of Ember.js so that each class had only one responsibility. This is the result of my experiment.
You can think of a software application as having three layers, each having a single concern.
The middle layer is the brains of the software. It contains the entire state of the system and the relevant logic.
The bottom layer is the software’s long-term memory. It preserves any data you would want to be remembered after the user has finished their session with the software.
My architecture experiment involved an app that had an Ember.js front-end and a Rails back-end. In this context, the display layer is the web page, the state layer contains Ember objects, and the persistence layer is Ember Data talking to the Rails back-end.
The Display Layer
I determined that there were four main concerns in the display layer.
The first concern is producing HTML for the browser to render to the user. Ember.js uses Handlebars Templates for this.
The third concern is that the user needs some way to interact with the software–to provide data or trigger an action. Ember.js gives us Controls to do this, such as Ember.TextField and Ember.Select.
The fourth concern is DOM-related behavior. This is behavior not related to the state of the software, but to the user interface. One example would be auto-focusing on a form element. Another would be adding a jQuery animation. Ember.js does not give us a class with this single responsibility, so I introduced the concept of a Pane.
The State Layer
Ember Data provides us with a nice DS.Model class for representing objects that are meant to be persisted. However, some application state does not need to be persisted, such as which tab is selected or a currently selected item in a list. To distinguish between these two types of objects, objects that are meant to be persisted I call Storables and objects that are used to remember the display state I call Viewables.
In Between the Layers
Ember Data takes care of the communication between the state layer and the persistence layer. But what about the communication between the state layer and the display layer?
When the state of the application changes, the display needs to be updated. Ember.js provides a wonderful mechanism for this, called Bindings. So Bindings are the mechanism for the state layer to affect the display layer.
For the display layer to affect the state layer, I created a new type of object called a Handler. Controls are always hooked up to a Handler. A Handler receives the action generated by the Control and, in response, changes the state of a Viewable or Storable. That’s all a Handler does: receives an action from the display, responds by changing state. Ideally, it has no knowledge of the DOM.
Tying It All Together
So what changes did I end up making to the architecture of Ember.js?
I also separated out the concerns of Ember.js Controllers. Ember.js doesn’t have a Controller class per se, but there is an idiom where Controllers have three concerns:
- holding a collection of Ember objects
- responding to actions coming from Controls by changing state
- creating calculated properties related to the collection
The todosController is a good example of a Controller having these three concerns.
I split up these concerns like so:
- holding a collection of Ember objects — I created a separate class for this. It felt very natural. The class contained the collection of Storables as well as related methods like create.
- responding to actions coming from Controls by changing state — I called this a Handler.
- creating calculated properties related to the collection — these properties, being state, would end up in either a Viewable or Storable class.
The important thing to notice is that each component of this architecture is responsible for only one thing, so that when you develop a new feature, it’s easy to decide what code goes where:
- Templates: hold the HTML
- Helpers: keep HTML DRY
- Panes: contain DOM-specific code
- Controls: provide a place for user input
- Handlers: initiate state changes based on user input
- Viewables: represent temporary application state
- Storables: represent state that should be persisted
- Bindings: communicate changes in state to the relevant listeners
But Will It Blend?
So what was the result of my experiment? Was it a success?
I only took the experiment so far, but as far as I got, it seemed to work. There are definitely some ideas in here worth considering. That said, I won’t say that it’s objectively better than what Ember.js provides out of the box.
It’s to Ember’s credit that I was able to tweak the architecture so easily. I didn’t have to modify any of Ember’s core classes, just play around with names and use reopen to split the Ember.View class between two files.
Splitting the Ember.View class seemed the least natural. Even though Ember.View contains both state and display concerns, there is a certain cohesiveness there. That said, it felt really clean to be able to think of the app in the clearly defined chunks of “display” and “state”. It made decisions of what goes where easier.
The Controller refactoring was one of the best wins. Controllers in Ember.js seem like the least cohesive part of the system.
All in all, it’s a matter of taste and scale. Taste, because this experimental architecture results in lots of little files. Some people don’t like to program that way; they don’t like constantly switching among many files. Scale, because you generally only see the benefits of splitting responsibilities out so finely when the number (and complexity) of features grows very large.
Recommendations For MVC-Inspired Architectures
In light of the recent Cambrian explosion of MVC-inspired web architectures, I have some advice for would-be architects.
#1: Be Intentional About Separation of Concerns
It’s not correct to say that you have to follow the Single Responsibility Principle all of the time. But if you don’t, it should be on purpose. Too many MVC implementations end up with a Controller class that is a soupy mess.
#2: MVC Does Not Mean Three Classes
An MVC-inspired architecture does not mean you have a Model class, a View class, and a Controller class. The architecture presented in this post is strongly MVC, yet has many more than three classes.
#3: Use Intent-Revealing Names
One of the most important skills of software architecture is good naming. Names should clarify the intent of the class, not obfuscate. What this means is that you should consider no longer using the names “Model”, “Controller” or “View” as class names. This is because these names have been used in so many different ways by so many different MVC architectures that when you use these names, you are asking a new developer to first unlearn a previous concept they already associated with that name. You are adding friction. Names should reduce cognitive friction.
But even if you can’t stomach a complete moratorium on the names Model, View and Controller, I claim that you should never use the name Controller. Your architecture design will be better for it. First of all, Controller is not an intent-revealing name in the first place. What, exactly, is it controlling? It’s a design smell, if you ask me. Backbone.js gets this right. It doesn’t have a Controller class; it doesn’t even claim to be MVC.
I hope you found this Ember.js architecture experiment useful and thought-provoking.