Site icon Shine Technologies

Rich Object Models and Angular.js

screenshot

Another sort of rich model

Angular.js is deliberately un-opinionated about how you should structure your data models. Whilst it lays out very clear guidelines for directives, controllers and services, it also makes a selling-point of the fact that it can bind to plain-old Javascript objects with plain-old properties.

Services are singletons, so it can be unclear as to if and how you can group per-object state and behaviours.  Consequently, people tend to fall back to services that deal in very simple JSON objects – i.e. objects that contain only data, not behaviour. However, building the sorts of rich interfaces that our users demand means that we sometimes need to more fully leverage the MVC pattern. Put differently, for some problems it can be useful to have a rich object model that provides both data and behaviour.

Recently at ng-conf, I presented a simple approach I’ve used for using rich object models with Angular.js. This technique leverages existing frameworks, maintains testability, and opens up a range of possibilities for more succinct and easy-to-understand code. In this post I’ll outline the underlying approach, but you can also find some (very simple) helper code here.

An Example

Let’s say that we have a web app that’s used for preparing proposals for fitting-out the interiors of fleets of private aircraft. Things like fancy seats, video and stereo systems, lighting, etc. As it turns out, calculating the costs, revenues and profits for such a fit-out is actually rather complicated. This is because the object model looks something like this: You can see that each proposal comprises a recurring engineering section, which describes the per-aircraft costs (for example, parts and installation labour), as well as a non-recurring engineering section, which describes the initial setup costs for a particular job (for example, scaffolding or facilities setup). We have material costs, which describe the costs of parts and other materials, as well as internal costs, which cover labour. These and all of our other costs and prices needed to be sliced and diced in various ways to calculate costs, revenues and profits for the proposal.

I won’t go into all the details here, but one additional point worth making is that all of the monetary amounts in this system can be in different currencies. For example, a proposal has an internal currency, which is what we use internally for accounting, and an external currency, which is the currency that the customer will pay with. Furthermore, all of our parts and labour costs can be in different currencies.

Loading Data

Say that we a have a back-end that can serve up data relating to proposals in JSON format from RESTful endpoints. Firstly we need a way to load data from the back-end to the client. I’m going to use Restangular for this. The code in our Angular controller might look something like this:

[sourcecode language=”javascript”]
angular.module(‘controllers’, [‘restangular’]).
controller(‘ProposalsCtrl’, function($scope, Restangular) {
// GET /proposals
Restangular.all(‘proposals’).getList().then(
function(proposals) {
$scope.proposals = proposals;
}
);
});
[/sourcecode]

However, putting all of this logic into a controller is generally considered bad form, so let’s push part of it into a service:

[sourcecode language=”javascript”]
angular.module(‘services’, [‘restangular’]).
factory(‘ProposalsSvc’, function(Restangular) {
return Restangular.all(‘proposals’);
});
[/sourcecode]

And simplify our controller by having it use this service:

[sourcecode language=”javascript”]
angular.module(‘controllers’, [‘services’]).
controller(‘ProposalsCtrl’, function($scope, ProposalsSvc) {
// GET /proposals
ProposalsSvc.getList().then(
function(proposals) {
$scope.proposals = proposals;
}
);
});
[/sourcecode]

Much better – but where does the business logic come into it?

Glad You Asked

The calculations that we want to perform for a proposal include things like cost, revenue and profit. Using the traditional Angular approach, this logic would be put into stateless services. Consequently, we could make it that the ProposalsSvc returns an object for both fetching proposals and performing calculations on them. The end result would be that the service looks something like this:

[sourcecode language=”javascript”]
angular.module(‘services’, [‘restangular’]).
factory(‘ProposalsSvc’, function(Restangular, MoneySvc,
RecurringEngineeringSvc, NonRecurringEngineeringSvc) {
return {
getProposals: function() {
return Restangular.all(‘proposals’);
},
profit: function(proposal) {
return MoneySvc.subtract(
this.revenue(proposal), this.cost(proposal)
);
},
revenue: function(proposal) {
return MoneySvc.convert(
proposal.price(), proposal.internalCurrency
);
},
cost: function() {
return MoneySvc.add(
RecurringEngineeringSvc.cost(
proposal.recurringEngineering
),
NonRecurringEngineeringSvc.cost(
proposal.nonRecurringEngineering
)
);
}
};
});
[/sourcecode]

Note that:

However, this stateless-service approach gets pretty unwieldy very quickly. For this sort of problem, the best place for us to put these calculations is on the model itself. But how do we do this?

Restangular To The Rescue

Fortunately, Restangular includes a method called extendModel that lets us decorate models returned from particular routes with additional behaviour. So lets go back to our original service, but add some configuration so that the models get new methods added to them:

[sourcecode language=”javascript”]
angular.module(‘services’, [‘restangular’]).
factory(‘ProposalsSvc’, function(Restangular) {
Restangular.extendModel(‘proposals’, function(obj) {
return angular.extend(obj, {
profit: function() {
return this.revenue().minus(this.cost());
},
revenue: function() {
return this.price().
convertTo(this.internalCurrency);
},
cost: function() {
return this.recurringEngineering.cost().plus(
this.nonRecurringEngineering.cost()
);
}

});
});

return Restangular.all(‘proposals’);
});
[/sourcecode]

That’s better!

A couple of things to note about this code:

Making it Testable

We need a way to unit-test these sorts of calculations in isolation, without having to mock up what Restangular does. But we’d also like to be able to keep the logic in the models rather than breaking it out into separate stateless services. One strategy for doing this is to move the methods into their own service:

[sourcecode language=”javascript”]
angular.module(‘models’).
factory(‘Proposal’, function() {
return {
profit: function() {
return this.revenue().minus(this.cost());
},
revenue: function() {
return this.price().
convertTo(this.internalCurrency);
},
cost: function() {
this.recurringEngineering.cost().plus(
this.nonRecurringEngineering.cost()
);
},

};
});
[/sourcecode]

Note how I’ve actually put this service into a new module called ‘models’. Let’s now use the Proposal model in ProposalsSvc:

[sourcecode language=”javascript”]
angular.module(‘services’, [‘restangular’, ‘models’]).
factory(‘ProposalSvc’, function(Restangular, Proposal){
Restangular.extendModel(‘proposals’, function(obj) {
return angular.extend(obj, Proposal);
});

return Restangular.all(‘proposals’);
});
[/sourcecode]

So now we’ve been able to separate the proposal logic into a separate service. However, the service is really just a simple object that can be mixed into other objects. To unit test the logic in this object, we can mix it into plain Javascript objects that contain the necessary data, rather than objects obtained from Restangular. Some things to note:

Nested Models

You may have noticed that Proposal.cost() includes references to this.recurringEngineering.cost() and this.nonRecurringEngineering.cost(). Where did those methods come from? Well, we pretty much need to take the same approach, and mix business logic into those objects too. The same can be said for this.internalCurrency and this.externalCurrency. At first, we might want be tempted to use the ProposalSvc to do this:

[sourcecode language=”javascript”]
angular.module(‘services’, [‘restangular’]).
factory(‘ProposalSvc’, function(Restangular) {
Restangular.extendModel(‘proposals’, function(obj) {
angular.extend(obj.recurringEngineering, {

});
angular.extend(obj.nonRecurringEngineering, {

});
angular.extend(obj.internalCurrency, { … });
angular.extend(obj.externalCurrency, { … });

return angular.extend(obj, Proposal);
});

});
[/sourcecode]

However, this approach doesn’t really scale – this.recurringEngineering and this.nonRecurringEngineering will contain properties of their own, each of which needs to have additional behaviour mixed into it, and so on.

I’ve found that the best approach is to adopt a convention where mixins have a method that allows them to mix themselves into an object. This convention can then be applied recursively down through the object hierarchy. So we tweak ProposalsSvc to look like this:

[sourcecode language=”javascript”]
angular.module(‘services’, [‘restangular’, ‘models’]).
factory(‘Proposals’, function(Restangular, Proposal) {
Restangular.extendModel(‘proposals’, function(obj) {
return Proposal.mixInto(obj);
});

});
[/sourcecode]

And add the mixInto method to the Proposal mixin:

[sourcecode language=”javascript”]
angular.module(‘models’).
factory(‘Proposal’, function(
RecurringEngineering, NonRecurringEngineering, Currency
) {
return {
mixInto: function(obj) {
RecurringEngineering.mixInto(
obj.recurringEngineering
);
NonRecurringEngineering.mixInto(
obj.nonRecurringEngineering
);
Currency.mixInto(obj.internalCurrency);
Currency.mixInto(obj.externalCurrency);
return angular.extend(obj, this);
},
profit: function() {
return this.revenue().minus(this.cost());
},

};
});
[/sourcecode]

Note how we’ve introduced additional RecurringEngineering, NonRecurringEngineering and Currency mixins that decorate the appropriate objects. Strictly speaking, it’s probably not necessary to copy the mixInto method into each target object. However, for now I’m keeping things simple in order to get the basic idea across.

Talk is Cheap, Show Me The Github Project

I have extracted some simple helper code that facilitates this technique and put it up on Github. There’s not much to it, but it does add some conveniences like:

However, for us to get this stuff, there are two trade-offs that we have to make:

So using this Base helper – which I’ve put in a module called shinetech.models, our Proposal mixin ends up looking like this:

[sourcecode language=”javascript”]
angular.module(‘models’, [‘shinetech.models’]).
factory(‘Proposal’, function(
RecurringEngineering, NonRecurringEngineering, Currency,
Base
) {
return Base.extend({
beforeMixingInto: function(obj) {
RecurringEngineering.mixInto(
obj.recurringEngineering
);
NonRecurringEngineering.mixInto(
obj.nonRecurringEngineering
);
Currency.mixInto(obj.internalCurrency);
Currency.mixInto(obj.externalCurrency))
},
profit: function() {
return this.revenue().minus(this.cost());
},

});
});
[/sourcecode]

Note how we continue to call mixInto, but no longer actually implement it. Instead, we do our customisation in the beforeMixingInto hook.

Note that Base has recently been renamed to BaseModel in the Github project to encourage a naming convention that avoids name clashes with services

Let’s Wrap This Up

In this post I’ve presented a simple strategy for decorating data models in your Angular app with behaviour and business logic. Furthermore, I’ve done it in a manner that remains testable and scales up to complex nested data structures.

There are a multitude of ways in which this strategy can be polished and improved, but fundamentally it comes down to a simple convention for using mixins. I’ve found that using this approach sets up a clear path to more sophisticated object-oriented data-modelling techniques. If you’re interested in learning more, refer to my posts on identity mapping and getter methods, or my check out my ng-conf presentation video.

Exit mobile version