Welcome back to Teatime! This is a weekly feature in which we sip tea and discuss some topic related to quality. Feel free to bring your tea and join in with questions in the comments section.
Tea of the week: Royal Milk Tea. In Japan, when they want black tea, they buy it powdered with milk powder already added so they don’t have to go buy milk. This stuff is super simple to make since it’s instant, but it tastes amazing. You can often find it at an Asian grocery store if you happen to have one in your area.
Today’s topic: Maintainable code with MV*
Today we’re venturing into the area of maintainability. There are a lot of design patterns out there to increase the maintainability of your code, but none are as widespread in the web world as MV*
What? MV*?
The MV[something] family of patterns (abbreviated here as MV*, where * is a wildcard character) consist of a Model, a View, and… something else. The most common types are as follows:
- MVC: Model, View, Controller. This is the classic model, the basic template that the others are following.
- MVVM: Model, View, View-Model
- MVP: Model, View, Presenter
Backbone, a popular MV* framework for Javascript, provides tools to create Models and Views, but leaves the third piece up to you. We’ll be having a look at the pieces in the next few sections here.
Models
The model contains the data the application needs to work. Generally, this is an Object in an OOP paradigm; you might have, say, a Product model that knows the description and price of a single product, plus can work out the sales tax given the country, or some such. This is independant of any particular presentation, and should be global to your application. The model layer contains the business logic pertaining to the domain; for example, a Car model knows it can only have four Tires attached, but a Truck might have 18.
The thing to keep in mind here is that the model is a layer like any other, and can contain more than just your specific object models. It can also contain data mappers that store the models into a database, services to translate models into a different format, data in the database held in a normalized format, and the code needed to reconstruct those model objects out of the normalized data. There’s a lot going on here, and all of it can contain business logic that needs to be tested.
Views
Views are responsible for providing an interface to the user to interact with the data. In a classic server-based web app, your view is HTML, maybe a little injected javascript. In a single-page Javascript app, you’ll have View objects that construct widgets on the DOM, plus the HTML that’s actually injected. In a Windows Presentation Foundation app, you’ll have some XML defining your view.
View-models
This confused me for the longest time until I realized what the name was trying to imply. The Model layer models your application; the View-Model is a model of your View. Everything you need to render a view — the data, the methods, the callbacks — live in a model, and your actual view — the DOM, or whatever — just renders what’s in the view-model. This is the pattern Knockout.js uses: the DOM is the view, and your methods bind to it from the view-model. You never write code to update the view, it just happens automatically.
Controllers
The controller represents the logic of the application (or page, in a multi-page js app). It determines what models are needed and what views should be displayed based on the context. For example, in ASP.NET, you might have a bit of HTML (your view) showing a record from the Users table (your Model). When you click the edit pencil, the view dynamically changes out to a form (a different view) showing the same record. When you click save, it swaps back. The controller contains this sort of logic.
In an MVP or MVVM setup, without a controller, often the logic is controlled via a REST-style “route”: one page has one view and one model and they compose directly, and to do anything with that page, you make another HTTP request which returns another view with another model.
Practical Example: Backbone
Let’s take a look at one Javascript library that’s become very popular in recent years. These code samples are from cdnjs, by the way, which is a great place to learn about Backbone. Backbone can be used in an MVC style very easily: your controller summons up views and models and composes the two together.[1] One model maps to one endpoint in the API layer, which serves up one model; for example, this might be a model for a user:
var UserModel = Backbone.Model.extend({ urlRoot: '/user', defaults: { name: '', email: '' } }); user.save(userDetails, { success: function (user) { alert(user.toJSON()); } });
You can see here how we extend the basic model to create our own, and only override what we want to use. The urlRoot tells it where to look for the API backend; the defaults tell us what properties we expect to see back as well as providing sensible defaults in case a new object is created during the course of our application.
A view might look something like this:
var SearchView = Backbone.View.extend({ initialize: function(){ this.render(); }, render: function(){ var template = _.template( $("#search_template").html(), {} ); this.$el.html( template ); }, events: { "click input[type=button]": "doSearch" }, doSearch: function( event ){ // Button clicked, you can access the element that was clicked with event.currentTarget alert( "Search for " + $("#search_input").val() ); } }); var search_view = new SearchView({ el: $("#search_container") });
Here we have an element ($el) we will bind our template to; that’s usually passed in when the view is instantiated. We also have a template here, in this case an underscore template. We then render the template when we’re asked to render.
We use Backbone’s events array to register for certain events on our element, and when the button is clicked, we perform an action (in this case, an alert).
Do you think this code is easier to maintain than the usual giant closure full of methods? Why or why not? Have you seen any maintenance nightmares recently you think could have been sorted out by using MV* better?
[1]: This assumes a REST backend, which we can talk about another day.