topics: functional programming, concurrency, web-development, REST, dynamic languages

Saturday, March 8, 2008

Client/Server Web-apps -- the model (Part I, Section II)

This article is Part I, Section II of a series discussing the development of client/server web-applications where the client is written in JavaScript and running in a browser. The first article discusses the use of namespacing in large JavaScript applications; it can be found here.

The next couple of articles will look at one way of using the Model-View-Controller (MVC) design pattern when structuring the client. In doing so, we will touch upon other topics: Crockford's singleton and module patterns, inheritance and the observer/observable design pattern. This article gives a highlevel view of the architecture and presents the Model-part of MVC.

Introduction.

In our webapp, the model-view-controller pattern is used in a manner close to MVC in non-web applications, e.g. Swing applications, which is quite different from the common MVC pattern for web-apps (e.g., as done in JSP Model 1 and Model 2 architectures, Rails, JSF etc.). For example, in the JSP Model2 architecture, the controller is a Servlet that receives a HTTP request, possibly manipulates the model (implemented as a collection of Java Beans), and finally dispatches to the view (implemented as a collection of JSP templates that consult model objects when rendering HTML).

Our server doesn't deliver any HTML. The server's responsibility is simply to implement a HTTP-based RESTful API for accessing resources (in fact, the server application itself is not structured by MVC, since it has no view in the traditional sense). The JavaScript client is decoupled from the server application and interacts with it only through the exchange of JSON data. Hence, our web-application is much more like a traditional distributed client/server application than a regular HTML-centric web-application. In fact, also our client application will be HTML-free! (I kind of like the name 'HTML-free' web-applications for this type of web-app). This sounds impossible, and actually it only mostly true ;-). However, using the JavaScript framework ExtJS, we are able to only use HTML for the 'lowest-level' custom functionality: all other GUI elements are composed of (JavaScript-) customized Ext Components and Containers. This let's us develop the view largely without thinking about HTML or ugly HTML-templates (of course Ext uses HTML internally).

So, it is the client that is structured by MVC. In fact, MVC can be implemented quite elegantly in JavaScript (I wonder if this is connected to the fact that MVC was originally developed for GUI applications written in Smalltalk, a language that in some ways is close to JavaScript).

We will use the singleton pattern for the controller, model and view, and so there are no constructor functions for these objects. Each of these are accessed as:

com.trifork.tribook.controller.Controller
com.trifork.tribook.model.Model
com.trifork.tribook.view.View


Their responsibilities are as follows:

  • Model represents the state of the application in terms of concepts from the application domain (Rooms and Reservations in our example). It is an observable object, which fires events when state changes.

  • View sets up all the GUI elements. It is also an observable object. The events fired by the view are not low-level events like DOM events; instead, they are logical events which are triggered by such low-level events, e.g., clicking on a button. An example logical event could be 'room reservation'.

  • Controller initializes model and view. It also listens on model and view events. For example, a controller component will be responsible for handling the logical 'room reservation' event fired by the view. Controller also manipulates the model (thus triggering more events) in response to, e.g., the result of Ajax requests.



An advantage of the event-driven programming model is that there is a loose coupling between the source of events and code that responds to such events. Furthermore, most of the time there can be several handlers for each event, and such handlers need not be aware of each other.

Recall that our example application, TriBook, is a simple room-booking application for reserving meeting rooms at Trifork. In the following we will look at the structure of the model-part of our TriBook application. We will be using the JavaScript framework ExtJS (the strongest JavaScript framework I know of) when defining the Model and associated domain concepts; also, we will use the namespace and using functions from the previous article.

The Model
The object: com.trifork.tribook.model is a so-called package object; it does not contain any logic, and serves only as a container for all objects and functions related to the model. In our TriBook application, this includes: Model (the model-object mentioned above), Room (a constructor function for the domain concept of a Room), Reservation (constructor for Reservation objects) and RoomQuery (constructor for objects representing a query for available rooms).

When defining domain constructor functions, we will use ExtJS, specifically: Ext.data.Record. This object has a method, called create, which is a higher-order function that returns a constructor function for a 'record' object, which are ideal for domain modeling for several reasons (as we shall see). This should make much more sense, when seeing the code:

namespace("com.trifork.tribook.model");
using(com.trifork.tribook.model).run(function(m){

var format = 'd/m/Y-H:i';
m.Reservation = Ext.data.Record.create([
{name: 'start_at', type:'date', dateFormat:format},
{name: 'end_at', type:'date', dateFormat:format},
//belongs_to room
{name: 'room'}//no 'type' means arbitrary object
]);
});

This code defines the Reservation domain concept: a reservation has a start and end time and refers to a Room object (the room being reserved). The 'dateFormat' property tells Ext how to parse a string to a Date object.

Since Ext.data.Record.create returns a constructor function, we can add domain logic to our domain objects by augmenting their prototypes. Below, we illustrate this with the Room domain concept which has a method 'isFreeFor' that returns true iff this room is available to the input RoomQuery object:

namespace("com.trifork.tribook.model");
using(com.trifork.tribook.model).run(function(m){
var rsvReader = new Ext.data.JsonReader({
root: "reservations"
}, m.Reservation);//converts JSON objects to Reservation objects

m.Room = Ext.data.Record.create([
{name: 'name', type:'string'},
{name: 'room_code', type:'string'},
{name: 'description', type:'string'},
{name: 'max_size', type:'int'},
{name: 'picture_url', type:'string'},
{name: 'map_url', type:'string'},

//Room has_many :reservations
{
name: 'reservations',

//this tells Ext how to convert a JSON array to
//an array of m.Reservation objects
//room is a reference back to the room object itself
convert: function(rsvs,room) {
Ext.each(rsvs, function(r){
Ext.apply(r,{room:room});//adds a room property to r
});
return rsvReader.readRecords({'reservations':rsvs}).records;
}
}
]);

//is this room free for input room query object?
m.Room.prototype.isFreeFor = function(query) {
return (query.get('min_size') <= this.get('max_size')) &&
! this.get('reservations').some(function reserved(period) {
//... details omitted
});
};
});

I think it is noteworthy how succinctly we can define domain concepts using Ext, e.g., {name: 'name', type:'string'} declaratively specifies that rooms have a 'name' property of type 'string'. Further, using Ext.data.Record objects as our domain objects we can leverage other Ext functions in several ways: for example Ext.data.JsonReader or Ext.data.XmlReader automatically converts JSON or XML data representations of domain objects to actual domain object instances. Furthermore, the Ext.data.Store abstraction gives us a client-local cache of domain objects; something we will be using in the Model object.

The observable/observer pattern. It should come as no surprise that we are also leveraging ExtJS in our use of the observable pattern: the com.trifork.tribook.model.Model objects is an instance of the type Ext.util.Observable. This gives us the ability to add named events which can be fired and all listeners notified. For example, we have the following (note that Ext.apply(A,B) copies all the properties of object B to object A):

namespace("com.trifork.tribook.model");
using(com.trifork.tribook.model).run(function(m){
//details omitted here...

m.Model = (function() {//inline
var model = new Ext.util.Observable();
model.addEvents('beforequery','query','beforerooms','rooms');

return Ext.apply(model, {
init: function(conf) {
this.rooms.set(conf.rooms);
},
//attr_accessor defined below
rooms: attr_accessor('rooms'),
query: attr_accessor('query'),
pendingReservations: attr_accessor({
name:'pendingReservations',
setter: false
})
});
})();
//details omitted here...
...

Ignore 'attr_accessor' for now, it simply creates a property on the Model object (if you know rails you can probably guess what it does) - we will get back to this later. Model is an observable object with events: 'beforerooms', 'rooms', 'beforequery' and 'query'. The client application always has a current query which represents the period in time that the user is presently interested in (represented as a form in the view). The controller makes sure that the query property of the model is updated when the user changes the form values, and hence the 'query' event is fired with the updated value as a parameter. Other view components, in turn, listen on this model property and react to these changes, which removes dependencies from the view and gives a looser coupling.

Using Ext, adding an event listener can be done as follows: m.Model.on('query', function(q){...});. When the 'query' event is fired on the model, the supplied function is called, binding an optional event parameter to q. Similarly, firing an event is simple: m.Model.fireEvent('query', new m.RoomQuery(...));. When an event is fired, all listeners are invoked in turn.

The full model source is:

/*extern com, Tri, Ext*/
namespace("com.trifork.tribook.model");
using(com.trifork.tribook.model).run(function(m){
var State = {
rooms: null,
query: null,
pendingReservations: new Ext.data.Store()
};

m.Model = (function() {//inline
var model = new Ext.util.Observable();
model.addEvents('beforequery','query','beforerooms','rooms');

return Ext.apply(model, {
init: function(conf) {
this.rooms.set(conf.rooms);
},
//attr_accessor defined below
rooms: attr_accessor('rooms'),
query: attr_accessor('query'),
pendingReservations: attr_accessor({
name:'pendingReservations',
setter: false
})
});
})();

//create an attribute accessor pair.
function attr_accessor(spec){
spec = typeof spec === 'string' ? {name: spec, setter:true} : spec;
var name = spec.name,
before = 'before'+name,
res = {
get: function(){
return State[name];
}
};
if (spec.setter) {
res.set = function(val){
var old_val = State[name];
if (m.Model.fireEvent(before,val,old_val) !== false){
State[name] = val;
m.Model.fireEvent(name,val,old_val);
}
};
}
return res;
}
});

Conclusion
At the end of this writing, I somehow feel that I've not completely expressed my points as clearly as I would have liked -- this paragraph is an attempt to remedy this. Since I am unsure how to re-write this posting more concisely, I'll just try and emphasize my main messages:

  • Our 'web-app' should be thought of as a regular distributed client/server app. The server uses RESTful HTTP, and is HTML free; it's primary format for resource representation is application/json. (All this is a topic of a later posting). The client is written JavaScript and is (almost) HTML free. The client is structured after MVC. Client and server constitute a HTML-free webapplication!

  • The MVC design is event based; this simplifies components and reduces coupling. The view manages the GUI and converts user actions to logical application events. Controller handles application events. Model represents the state of the (client) application; it fires events when state changes.

  • The package object com.trifork.tribook.model contains domain concepts (e.g. Room); it also contains the actual Model object. The state of Model is represented via the domain concepts.

  • I highly recommend ExtJS in developing this type of client application. Ext is extremely useful in M, V and C; for this posting we focused on M, where the Ext features support for:

    • concise definition of domain concepts using Ext.data.Record

    • client caches (collections) of domain objects, asynchronously linked to a server backend, i.e., Ext.data.Store, Ext.data.HttpProxy

    • observer/observable design pattern: Ext.util.Observable

    • XML and JSON parsing and mapping to domain concepts (Ext.data.JsonReader, Ext.data.XmlReader)

    • and of course Ext itself is highly extensible and event based





No comments: