I have always advocated in favor of decoupled object models for testability and flexibility, but what does that really mean? In this post I will, through an example, demonstrate what I mean by a decoupled object model and highlight the major benefits of the approach.

JavaScript coding isn't what it used to be. I remember the days when JavaScript was nothing but a big mess of disorganized script tags and no clear direction in terms of architecture or modeling. jQuery was introduced and gave the entire community a big productivity boost, but offered very little in terms of code organization and separation of concerns. Luckily separation of concerns has in recent years become much more of a focus in the JavaScript community. Model-View-X frameworks like Angular and Knockout have been introduced and given us well organized patterns to follow. However these new frameworks only encourage separation of concerns, but don't require it, so there is still a lot of room for falling back on old sins.

In the following example I want to demonstrate how to write JavaScript where the object model is properly decoupled from the DOM in a way that makes it both testable and flexible. This particular example is written in Angular, but truthfully, this is not a concept tied to Angular. Rewriting the entire thing in Knockout or some future framework (Angular 2.0...) will not really change much except some wrapper syntax.

The example I will be using is a simple shopping cart.

The first order of business is to look at the general concept of a shopping cart and break it down into domain objects that can be used to describe the cart and its operations. For brevity I have decided to limit the functionality of the cart to adding, removing and calculating the total price of items in the cart.

As you can tell from the code below I have created two domain objects – shoppingCart and shoppingCartItem. The shoppingCart model contains methods for adding, removing and calculating the total price, but notice how it delegates the actual calculations to the individual items. Separating this logic is important since it enables to to support complex logic at the item level without having to modify the cart. As you may have guessed, this sets us up to support polymorphism if we in the future want to go with different types of item models. The beauty of the solution is that any future complexity in price calculations will be handled at the item level without any need for branching on items types in the cart model.

Shopping cart items are totally unaware of the shoppingCart, and how a group of items are organized – making this a one way dependency chain without unnecessary couplings. Ideally a fully decoupled object model should only know how to carry out tasks specific to its domain, and be ignorant of how it's being used in a bigger picture. We are taking advantage of this when writing unit tests. Properly decoupled models should be just as easy to use from a unit test as from a markup view.

Shopping cart code:

angular.module('app').factory('shoppingCartFactory',[function(){ //shopping cart model function shoppingCart(){ this.itemsToPurchase = []; }; shoppingCart.prototype.addItem = function(data){ this.itemsToPurchase.push(new shoppingCartItem(data)); }; shoppingCart.prototype.calculateTotalPrice = function(){ this.totalPrice = 0; for(var i = 0; i < this.itemsToPurchase.length; i++){ this.totalPrice += this.itemsToPurchase[i].calculatePrice(); } }; shoppingCart.prototype.removeItemById = function(id){ for(var i = 0; i < this.itemsToPurchase.length; i++){ if(this.itemsToPurchase[i].id === id){ this.itemsToPurchase.splice(i,1); break; } } }; //shopping cart item model function shoppingCartItem(data){ this.id = data.id; this.price = data.price; this.productName = data.name; this.discount = data.discount; } shoppingCartItem.prototype.calculatePrice = function(){ return this.price - this.discount; }; function createShoppingCart(){ return new shoppingCart(); } return {createShoppingCart:createShoppingCart}; }]);

As you will see in the unit test, the functionality is completely encapsulated in the object model, but it makes no assumption about how it's being used. Very different markup views or unit tests can use the same exact model without requiring modifications to the model. Benefits of refusing the same model across different views are obvious, but the impact on unit testing is huge here since we can fully test the whole feature without a dependency on the DOM.

The test below shows how we can realistically unit test a full cycle of user interactions of adding and removing items with updated price calculations.

describe('shoppingCart total price', function(){ var shoppingCart; beforeEach(function() { module('app'); inject(function ($injector) { shoppingCart = $injector.get('shoppingCartFactory').createShoppingCart(); }); }); it('should calculate the total price of changing items in shopping cart', function(){ shoppingCart.addItem({id:'1', price:8, name: 'Candy bar', discount:5}); shoppingCart.addItem({id:'2', price:5, name: 'Ice cream', discount:0}); //calculate price after adding initial items shoppingCart.calculateTotalPrice(); expect(shoppingCart.totalPrice).toBe(8); //remove item and recalculate shoppingCart.removeItemById('1'); shoppingCart.calculateTotalPrice(); expect(shoppingCart.totalPrice).toBe(5); //add item and recalculate shoppingCart.addItem({id:'3', price:20, name: 'Apple Pie', discount:10}); shoppingCart.calculateTotalPrice(); expect(shoppingCart.totalPrice).toBe(15); }); });

It's of course recommended to include tests for the individual operations in isolation, but for brevity I am only including a test of a full sequence of user interactions working in concert.

The main purpose of this post has been to illustrate how to write properly decoupled object models. Hopefully this will give people a better idea of the benefits of breaking the tight coupling between code and DOM. The example I'm using is probably too simple to do the approach complete justice, but in a much more complicated model with a deeper hierarchy, this approach really shines. I have benefited from this architecture numerous times. It's also worth reminding everyone that this is really all about JavaScript modeling and mostly framework agnostic.