Rich Object Models and Angular.js

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: Proposal Tool Object Model 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:

angular.module('controllers', ['restangular']).
  controller('ProposalsCtrl', function($scope, Restangular) {
    // GET /proposals
    Restangular.all('proposals').getList().then(
      function(proposals) {
        $scope.proposals = proposals;
      }
    );
  });

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

angular.module('services', ['restangular']).
  factory('ProposalsSvc', function(Restangular) {
    return Restangular.all('proposals');
  });

And simplify our controller by having it use this service:

angular.module('controllers', ['services']).
  controller('ProposalsCtrl', function($scope, ProposalsSvc) {
    // GET /proposals
    ProposalsSvc.getList().then(
      function(proposals) {
        $scope.proposals = proposals;
      }
    );
  });

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:

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
          )
        );
      }
    };
  });

Note that:

  • All of our monetary amounts are calculated using methods like add, subtract and convert on a separate MoneySvc service. This is because monetary amounts are objects with currencies rather than just numeric amounts.
  • To make unit testing easier, we also introduce additional RecurringEngineeringSvc and NonRecurringEngineeringSvc services to calculate values for the corresponding sectionvices, and so on

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:

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');
  });

That’s better!

A couple of things to note about this code:

  • We use angular.extend to copy (or mix-in) methods into each proposal instance that Restangular creates for us. This means that each proposal can have things like the profit, revenue and cost calculated directly on it.
  • The methods that we’re mixing-in assume that the proposal object will already have properties like internalCurrency, recurringEngineering and nonRecurringEngineering available on it.

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:

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()
        );
      },
      ...
    };
  });

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

angular.module('services', ['restangular', 'models']).
  factory('ProposalSvc', function(Restangular, Proposal){
    Restangular.extendModel('proposals', function(obj) {
      return angular.extend(obj, Proposal);
    });

    return Restangular.all('proposals');
  });

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:

  • It’s very important that we use angular.extend(obj, Proposal) rather than angular.extend(Proposal, obj). If we use the latter, then the original Proposal service will have properties written into it. This would be a bad thing. However, there are strategies that can be used to avoid accidentally doing this that I’ll discuss later.
  • Strictly speaking, the Proposal mixin probably doesn’t even need to be an Angular service – it could just be a plain Javascript object that we mix in. However, for the sake of consistency we’ll keep it as a service.
  • This approach doesn’t copy to the object prototype, because: 1. I was reluctant to mess with the prototype of the object provided by Restangular; and 2. The copy happens every time extendModel gets called, which would kind of defeat the purpose of using a prototype. However, as a result it may not be as efficient. There’s probably some scope to improve this.

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:

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);
    });
    ...
  });

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:

angular.module('services', ['restangular', 'models']).
  factory('Proposals', function(Restangular, Proposal) {
    Restangular.extendModel('proposals', function(obj) {
      return Proposal.mixInto(obj);
    });
    ...
  });

And add the mixInto method to the Proposal mixin:

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());
      },
      ...
    };
  });

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:

  • Giving us a mechanism for easily and safely extending mixins
  • Automatically calling angular.extend when we call mixInto
  • Ignoring attempts to mixin to null objects (something that will kill angular.extend). This is useful if you want to use a mixin in different situations where child properties may or may not be present.

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

  • We have to extend mixins from a base object. Note that this is not a base class – we can continue to mix the resultant objects into plain JSON objects we get from Restangular, etc.
  • Instead of implementing mixInto ourselves, the base object has to provide it for us. Consequently, we now add additional mixin behavior via an optional beforeMixingInto hook that will be invoked for us as part of the mixin process. This hook will be passed the same arguments that were passed to mixInto.

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

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());
      },
      ...
    });
  });

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.

Tags:
ben.teese@shinesolutions.com

I'm a Senior Consultant at Shine Solutions.

17 Comments
  • Rob Pocklington
    Posted at 13:53h, 04 February Reply

    Great article Ben.

    Mixins are a great solution for reusable business logic and are easy to implement in Javascript.

  • Eric Miller
    Posted at 03:20h, 25 February Reply

    This is really niiiiice. Seems like Angular is hurting for a rich model layer and this code solves many of the problems I have.

  • plalx
    Posted at 00:50h, 14 March Reply

    Great article! However, ss there any reason why you chose a mix-in approach rather than relying on prototypes to add behavior to the models? For instance, can’t Proposal.mixInto(obj) also be implemented using a classical approach, such as new Proposal(obj) or a pure prototypal approach like Object.create(Proposal).init(obj); ? It seems to me that it would be far more memory-efficient, no?

  • plalx
    Posted at 00:53h, 14 March Reply

    Great article! Is there any reason why you chose a mix-in approach rather than relying on prototypes to add behavior to the models? For instance, can’t Proposal.mixInto(obj) also be implemented using a classical approach, such as new Proposal(obj) or a pure prototypal approach like Object.create(Proposal).init(obj); ? It seems to me that it would be far more memory-efficient, no?

    • Ben Teese
      Posted at 12:59h, 14 March Reply

      Great question.

      The objects being decorated are created by Restangular, and contain some Restangular-specific methods.

      In order to retain access to these methods, I wanted to keep using these objects. I was also reluctant to mess around with the object prototypes, although this was more in the interests of keeping things simple.

      I was speaking with Martin Gonto (the creator of Restangular) recently and he mentioned the possibility of introducing more clearly-defined base classes to Restangular. This could in-turn make it easier to for me to extend upon these classes. It might also tie-in with the fledgeling work that is being done in Angular 2.0 on a persistence layer and base model class.

      That said, until the dust settles on those activities and/or I encounter serious and measurable performance issues, I’m happy to stick with a mixin approach for now.

      • Alexandre Potvin Latreille
        Posted at 00:36h, 15 March

        I wasn’t aware that they were working on a persistence layer for Angular, that’s very good news! As for the Restangular-specific methods, you could probably declare your own Model class which would delegate the Restangular method calls to the encapsulated obj. Your concrete model classes like Proposal would then just have to inherit Model. It might not have a big performance impact, but it would certainly reduce the memory consumption (think mobile). I created a test case to compare extend vs prototypes and it seems that using a constructor (Constructor using extend) performs slightly better than purely extending (Pure extend). I guess the difference would be greater with a bigger API, but I also only tested in very old browsers…

        Test case: http://jsperf.com/extend-to-prototypes

  • Ronald Haring
    Posted at 18:14h, 21 March Reply

    Nice post, but wouldnt it be more convenient to let the objects of restangular just be DTO’s and put the business logic in your own models? I think that would be my approach for this issue. Let the service do the talking to the backend and receive the dto’s to populate the business models and then let that service return the filled business models. That way all your business logic is in your models without any prior kwowledge of where that data is coming from or how it gets populated.

    • Ben Teese
      Posted at 14:18h, 24 March Reply

      Hi Ronald,

      Wouldnt it be more convenient to let the objects of restangular just be DTO’s and put the business logic in your own models

      I guess that would be a different approach, but would it involve any less code?

      That way all your business logic is in your models without any prior kwowledge of where that data is coming from or how it gets populated.

      I don’t think the mixins have any knowledge of where the data is coming from either, or am I missing something in what you’re saying?

      Cheers,

      Ben

    • Alexandre Potvin Latreille
      Posted at 23:19h, 24 March Reply

      I see no difference between the approach used in this article and what you describe.

      The mix-ins could be applied on any objects as long as they respect the interface contract (e.g. a Proposal must have a recurringEngineering property). The model decoration also occurs in the service, which removes that burden from the client.

      I think what confuses you is the fact that a mix-in approach was used rather than a classical one. Have a look at my previous comments for more information on that point.

    • Viktor Smirnov
      Posted at 02:02h, 31 October Reply

      I like your DTO/service approach and it feels cleaner. I am not a fan of Rich Models as they quickly grow out of proportion.

  • ARFR (@61726672)
    Posted at 03:02h, 26 May Reply

    God bless you, for the tip with Restangular!!!

  • Marco
    Posted at 22:09h, 19 July Reply

    This is great but my use case is to also give my user one screen to build a rich model on the fly and once they are satisfied save the object graph so this assumes a lot of collections and properties are already there. Can this impl handle this?

  • Lance
    Posted at 01:58h, 31 July Reply

    Thanks for the post. Any chance you can put together an example of how you use the last code snippet in an app? Not sure if you got rid of the service or not, the way I read the article.

  • twittstrap
    Posted at 05:28h, 03 August Reply

    Great article, I want to also recommend Twittstrap resources that includes a lot of useful Angular resources and tutorials

  • AJ (@zquad)
    Posted at 08:26h, 24 November Reply

    Ben Teese what approach would you take to add methods to models of the collection as opposed to the general collection itself?

    • Ben Teese
      Posted at 10:03h, 27 November Reply

      I think you can do something like that by iterating over the collection and mixing something into each element:

      beforeMixingInto: function(obj) {
        ...
        angular.forEach(obj.someCollection, function(element) {
          SomeModel.mixInto(element);
        });
        ...
      

      })

  • William Grasel
    Posted at 12:54h, 23 March Reply

    following this path, any suggestions for working with data validation?

Leave a Reply

Discover more from Shine Solutions Group

Subscribe now to keep reading and get access to the full archive.

Continue reading