Scaling Isomorphic Javascript Code
Take a minute and think about how often you've heard the phrase "Model-View-Controller" (or MVC). Do you really know what it means? At a high-level it is about a separation of concerns between the major areas of functionality in presentation-centric applications built on retained graphics systems (i.e not-raster graphics, such as games). Dig a little deeper and it becomes obvious that it is just a bucket term for a lot of different things. In the past, most development communities built-out an MVC solution that worked well for their most popular use-case and moved on. Great examples of this are the Ruby and Python communities with the MVC-based architecture Rails and Django both embody.
This approach has been acceptable for other languages such as Java, Ruby, and Python it is simply not good enough for Node.js for one reason: Javascript is now an isomorphic language. By isomorphic we mean that any given line of code (with notable exceptions) can execute both on the client and the server. On the surface this seemingly innocuous property creates a number of challenges that are not solved by current MVC-based patterns. This article will explore some of these existing patterns, how both their implementation and concerns vary across languages and environments, and how they are not good enough for a truly isomorphic Javascript codebase. In conclusion, we will explore a new pattern: Resource-View-Presenter.
At a glance
Design patterns are the bread and butter of application development. They encapsulate and outline the concerns of the application and the environment of the in which it exists. Between the browser and the server these concerns can vary widely:
- Is the view ephemeral (e.g. on the server) or long-lived (e.g. in the browser)?
- Is the view reusable across different use-cases or scenarios?
- Should the view be annotated with application-specific tags or markup?
- Where should the bulk of the business logic reside? (in the model? in the controller?)
- How is the application state persisted or accessed?
Lets explore some of the existing patterns and how they answer these questions:
- Model-View-Controller
- Model2
- Model-View Presenter and Model-View-ViewModel
- Modern Javascript Implementations
- Real-time Implications
- tl;dr? Introducing Resource-View-Presenter
- Conclusion
MVC
Figure 1: Model-View-Controller
The traditional Model-View-Controller pattern assumes a long-lived view and a swappable Controller. For example, in a given view you may have different Controller logic depending on who is logged in. At a high-level no decision is made about how the view is rendered (i.e. what templating engine is used).
Given the assumption of a long-lived view and that user interaction is by definition coming into the view, traditional MVC is a very useful pattern for front-end development. As we will explore later, a slightly modified version of this pattern is in fact whatBackbone.js uses.
Model2
Figure 2: Model2 Model-View-Controller
Don't be scared if you've never heard of Model2 before; it is a design pattern that dates back to 1999 when it was coined in an article by Govind Seshadri: Understanding JavaServer Pages Model 2 architecture. It can be argued that Model2 does not necessitate a fully-realized MVC pattern, but most modern implementations (such as Ruby on Rails) do formalize it in that way.
Figure 3: Rails Model2 Model-View-Controller
The common wisdom is that in Model2-like frameworks such as Ruby on Rails one should have "fat models and thin controllers". This is not the case for every application, but in practice it is generally what this author has seen. In the traditional MVC Controllers tend to be heavier (i.e. more business logic) due to their need to listen and react to input from the view so this decision seems to add up.
Given the stateless nature of HTTP the Model2 View is truly ephemeral: no state maintained in the view itself between requests.In most server-side frameworks any application state is stored via Session Cookies. This makes the decision of a one-way communication between the Controller and View very logical, but also unsuitable for any front-end development.
MVP and MVVM
Both the Model-View-Presenter and Model-View-ViewModel patterns are similar to the traditional MVC pattern with a few key differences:
- View does not have a direct reference to the Model
- Presenter (or ViewModel) has a reference to the view and updates it based on changes to the model
The MVP pattern is discussed at length by Martin Fowler (here, and here) and is generally discussed in the context of two implementations:
- Passive View: Design the view to be as naive as possible with the all but the absolute necessary presentation and business logic contained in the Presenter.
- Supervising Controller: Used in systems in which the declarative view can be expanded to encompass simple logic. In this incantation the Presenter should only take over when the declarative logic in the view cannot meet the system requirements.
Figure 4: Model-View-Presenter
Figure 5: Model-View-ViewModel
MVP and MVVM are almost indistinguishable with one key exception: MVVM assumes that changes in the ViewModel will be reflected in the view by a robust data-binding engine. Niraj Bhatt outlined the difference eloquently in his MVC vs. MVP vs. MVVM article: "For instance if View had a property IsChecked and Presenter was setting it in classic MVP, in MVVM ViewModel will have that IsChecked Property which View will sync up with."
The positive end of the MVP and MVVM is that the Presenter (or ViewModel) is easier to unit-test because the state of the View is by definition contained in methods invoked by (MVP) or properties set on (MVVM) by the Presenter or ViewModel respectively.
For front-end development either of these patterns are perfectly acceptable choices. A routing layer can pass control to the appropriate Presenter (or ViewModel) which in-turn can update and respond to a persistent view in the browser. With some massaging either could also be re-worked for use on the server for one reason: there is no connection between the model and the view. This allows for an ephemeral view which is rendered by a given Presenter (or ViewModel). As we will see later on, this modified pattern can be truly isomorphic.
Modern Javascript Implementations
The design patterns presented above have many modern implementations today:
These frameworks are generally used for creating Single-Page Applications (although more traditional AJAX is also possible). User Interaction within a single-page application comes in two distinct flavors:
- OnHashChanged or pushState events: Occur when the URL in the browser changes. For example: navigating tohttp://myapp.local/#/some-page
- DOM events: Occur when a user makes a specific interaction with the current DOM. For example: clicking on an anchor tag.
Lets consider some of the patterns and architectures used. If you're interested in further reading check out Peter Michaux's article on MVC Architecture for Javascript Applications.
Backbone
Backbone.js is one of the most popular client-side development frameworks available today. At it's core it is an implementation of the traditional Model-View-Controller pattern. When we examine it in more detail, however, we can see that there are some deviations from the traditional MVC pattern explored earlier.
Figure 6: Backbone Model-View-Controller
In the diagram above we have separated the control-flow represented by OnHashChanged and DOM events to illustrate the separate entry points offered by Backbone. By illustrating this nuance it is clear that Backbone has one important difference from traditional MVC: the View manipulates the Model. When we examine the Backbone Todo Sample, we can see this is clearly the accepted common wisdom:
window.AppView = Backbone.View.extend({
// ....
//
// When a TodoView is instantiated it is passed an
// instance of the Todo model.
//
addOne: function(todo) {
var view = new TodoView({model: todo});
this.$("#todo-list").append(view.render().el);
}
// ....
});
window.TodoView = Backbone.View.extend({
// ....
//
// Here the TodoView updates the state of the Todo model.
// This breaks from the traditional MVC in which the view
// only listens for changes on the Model.
//
toggleDone: function() {
this.model.toggle();
}
// ....
});
The decision to break from the traditional MVC pattern gives large Backbone applications all similar feel: thin controllers and models combined with heavy views. These business logic-heavy views are essentially Presenters when looked at objectively. In a large Backbone codebase you should expect a large number of views composited together via a DOM framework like jQuery or zepto.
There is nothing wrong with breaking from the traditional MVC pattern; in the context of front-end development having a reference to the Model from the View removes a lot of bookkeeping logic from the application. It does, however, make it a pattern that is not isomorphic.
Batman
Batman.js is a new Javascript framework that was discussed at JSConf 2011. Although the entities within Batman are Model, View, and Controller. The presence of a strong data binding engine and pure HTML views suggests that it is actually an implementation of the Model-View-ViewModel.
Figure 7: Batman Model-View-ViewModel
Having not worked with Batman much it is difficult to say with confidence what a large Batman codebase looks like. That said: the emphasis on the binding engine and thin views suggest that business logic will end up spread between the Controllers and Models within a given application.
As with Backbone, Batman breaks from the traditional Model-View-ViewModel pattern in that the Model communicates directly with the View and the ViewModel (i.e. Controller) does not manipulate the View directly. In addition, because of the references between the Model and the View it is not easily reusable as a server-side pattern. It is, however, more easily massaged into a server-side pattern if a Composite or Adapter pattern is implemented in the Model layer to render a static view and respond to real-time requests.
Real-time implications
Among all of the developer buzz one topic continues to be at the forefront: realtime web applications. Nodejitsu is an official sponsor of the upcoming Keeping it Realtime conference in Portland; check it out! So how do some of the patterns we've examined stack up for realtime support (such as WebSockets)?
- Model-View-Controller (Yes): Model(s) can listen for real-time events and update the view appropriately.
- Model2 (No): The Ephemeral View concept is baked into the Model2 pattern. That is: Controller(s) do not listen for events from the Model.
- Model-View-Presenter (Yes): Model(s) can listen for real-time events, which will propagate to the Presenter(s) who will update the view appropriately.
- Model-View-ViewModel (Yes): Model(s) can listen for real-time events, which will propagate to the ViewModel(s) who will update the view appropriately.
The acceptability of MVC, MVP, and MVVM patterns make Backbone.js or Batman.js workable realtime frameworks for front-end developments. On the server-side this fact no longer holds true because as we have shown: traditional MVC, MVP, and MVVM patterns will not work for static views due to the tight coupling between the View and the Model.
Enter the Resource-View-Presenter pattern.
Introducing Resource-View-Presenter
As we have shown: the MVC, MVP, and MVVM patterns will not work on both the client and the server. At the crux of Resource-View-Presenter is the realization that no pattern will work flawlessly both on the client and the server without some modification. As was suggested when examining the MVP and MVVM patterns, it is possible to make these patterns truly isomorphic due to their decoupled Model and View layers.
The main decisions which Resource-View-Presenter are:
- Decouple the Model and View
- Recognize and plan for the differences between the Client and Server.
- Expect a thin View, and a heavy Presenter and Resource.
- Prefer business logic in the Resource over the Presenter.
- Allow for both ephemeral (i.e. static server-side views) and persistent Views (i.e. the DOM).
- Prefer a Presenter over a ViewModel to preserve the purity of the Markup (i.e. HTML).
- Assume a persistent Presenter and Model.
Although these decision may seem as though they are random, each has a specific purpose:
- By decoupling the Model and the View we allow for both ephemeral and persistent Views
- A thin View is consistent with modern, logic-less templating engines such as weld and mustache.
- Having a Presenter instead of a ViewModel is consistent with designer-friendly templating such as weld.
- Assuming a persistent Presenter and Model both on the Client and Server enables encapsulation of real-time functionality in Presenters on both sides.
Lets dig deeper. On the Client-side Resource-View-Presenter resembles a traditional MVP pattern. The choice to rename the Model to Resource is heavily influenced by our assumption to prefer business logic in the Resource over the Presenter. This makes the Resource in RVP more similar to the heavy Models found in Model2, not traditional MVP the nomenclature denotes that. When implementing RVP the litmus test for what logic belongs in the Presenter is two fold: any Presentation-specific logic which is too heavy for a "thin" View or any business logic which utilizes global application state.
As with Backbone and Batman, Client-side RVP implementations should support both OnHashChange / pushState routing and DOM events.
Figure 8: Client-side Resource-View-Presenter
On the Server-side Resource-View-Presenter is almost identical with one notable exception: the View is ephemeral and does not pass calls or have a reference to the Presenter. In fact, when using RVP for JSON-based web-services the view is practically non-existent; it is just a call to
JSON.stringify()
. Figure 9: Server-side Resource-View-Presenter
At first glance, Server-side RVP may seem similar to Model2 MVC, but the persistent-nature of both the Presenter and Model to support real-time events makes this similarity is only skin deep. RVP does this by listening for events and changes on the model which may be backed by a real-time data source such as Redis PubSub or CouchDB changes.
The real-time support is of particular note because it enables the application developer to focus on the business logic, not the underlying network transport. This may seem trivial, but examine the pattern (or lack there-of) offered by Express and Socket.io(the most popular node.js Framework and Realtime IO libraries respectively) and it becomes clear that it is not.
Figure 10: Express and Socket.io
This is not meant to be an attack on Express or Socket.io; they both are explicit in what they offer and they both do it exceptionally well. A higher-level design pattern is simply not a concern.
Conclusion
Writing large applications is hard. Encapsulating and reusing components of those applications across the Client and the Server is even harder. After considering this analysis it is hopefully clear how utilizing the Resource-View-Presenter pattern within your own applications will help to make this easier.
Комментариев нет:
Отправить комментарий