Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

Rich Object Models and Angular.js: Identity Maps

DZone's Guide to

Rich Object Models and Angular.js: Identity Maps

· Java Zone
Free Resource

Build vs Buy a Data Quality Solution: Which is Best for You? Gain insights on a hybrid approach. Download white paper now!

 

angularIdentityMap

In my previous post on Rich Object Models and Angular.js I introduced a simple strategy for setting up rich object-models in Angular.js. It turns out that once we’ve introduced the notion of a rich object-model, a number of more advanced object-oriented programming techniques become easy to implement. The first of these that I’m going to discuss is identity maps.

In this post I’m going to talk about what an identity map is, why you might need one, and I’ll introduce a simple identity map implementation for Angular.js. I’m going to assume you’ve already read my foundational post. If you’d like to see the portion of my talk at ng-conf devoted to identity maps, jump to the video.

What’s an Identity Map?

An identity map typically maps a class/ID pair to an object instance. Identity maps are useful for ensuring that for each business entity in your system, there will only ever be one object instance representing that entity.

Identity maps are commonly used in object-relational mappers to ensure that for each table row in a database, there’s only ever one corresponding object. In Javascript single-page applications it can also be useful to have an identity map on the client-side to ensure that when JSON relating to particular business entity gets received from the backend, only one actual object instance is ever used to represent that entity in business logic.

Client-side rich-model frameworks like Breeze.js, Ember Data and Backbone Relational all contain identity maps. My colleague Greg Gross even implemented an excellent stand-alone identity map for Backbone.js a while back. Of all of these frameworks, only Breeze could be used with Angular, and even that might be overkill for many sorts of problems.

An Example

If all of this seems a bit academic, consider the following real-world example, taken from the Aircraft Proposal web app that I introduced in my previous post. Let’s revisit the object-model for that app, but place emphasis on a particular aspect of the model:

Proposal Tool

I’ve coloured the currency objects in red. Every monetary amount in the system has a currency. Currencies are of interest because fluctuations in their relative values can affect profits. For example, if the currency that the customer pays with (the external currency) varies significantly against the currency used internally to pay for the bulk of the labour and materials for the proposal, a loss could be made.

To model the effect that currency variations have on the proposal, it can be useful to actually tweak the currency exchange rate, then recompute profit and loss calculations for the proposal. However, if we have multiple objects representing the same currency, this can be hard to do. For example, if all labour and parts costs were in Euros, but the price to the customer was in US Dollars, to model a variation in the Euro we’d have to find every object instance representing the Euro currency in the system and tweak its value.

An easier approach is to guarantee that there’ll only ever be one object representing the Euro currency, and then tweak the exchange rate for that object. This is where an identity map becomes useful.

Back to the Map

So to be clear, an identity map maps an class/ID pair to an object instance. In our particular example, the contents might look something like this:

identityMap

Note that:

  • We’re just using strings to identify the ‘classes’. There are more sophisticated ways to accomplish the same thing, but I’ll keep it simple for now.
  • We can also put other types of objects in the map – for example, departments. However, the remainder of this demo will focus on currencies.

An Angular Identity Map

Implementing an identity map with Angular.js is relatively straightforward, the simplest approach simply being to write a factory service that returns a function:

angular.module('shinetech.models').factory('identityMap', 
  function() {
    var identityMap = {};
    return function(className, object) {
      if (object) {
        var mappedObject;
        if (identityMap[className]) {
          mappedObject = identityMap[className][object.id];
          if (mappedObject) {
            angular.extend(mappedObject, object);
          } else {
            identityMap[className][object.id] = object;
            mappedObject = object;
          }
        } else {
          identityMap[className] = {};
          identityMap[className][object.id] = object;
          mappedObject = object;
        }
        return mappedObject;
      }
    };
  }
);

You’ll find the full implementation and tests in the angular-models project on Github. A couple of things to note:

  • We’re always assuming that objects are identified by an id property
  • If an object has previously been loaded into the map, but another object with the same ID is presented to the map, it’ll merge the properties from the new object into the old one – then return the old one.
  • We don’t deal with the case where an object hasn’t got an ID yet. There are strategies for handling this (see Greg Gross’s Backbone Identity Map for an example), but we’ll keep it simple for now.

Using the Identity Map

Now let’s see how this identity map fits in with the mixin approach to rich object models. The key is to use the identityMap function at the point where deserialized objects and their children are being decorated with business behaviour. If you’re dealing with an object that has been previously identity-mapped, this is the point where you substitute in the original object.

Consider the internalCurrency and externalCurrency objects that are decorated when mixing Proposal behaviour into an object. To refresh your memory from my last post, the code originally looked 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());
      },
      ...
    });
  });

To identity-map the currencies, we’d change it as follows:

angular.module('models', ['shinetech.models']).
  factory('Proposal', function(
    RecurringEngineering, NonRecurringEngineering, Currency,
    Base, identityMap
  ) {
    return Base.extend({
      beforeMixingInto: function(obj) {
        RecurringEngineering.mixInto(
          obj.recurringEngineering
        );
        NonRecurringEngineering.mixInto(
          obj.nonRecurringEngineering
        );
        angular.extend(proposal, {
          internalCurrency: identityMap('currency',
            Currency.mixInto(proposal.internalCurrency)
          ),
          externalCurrency: identityMap('currency',
            Currency.mixInto(proposal.externalCurrency)
          )
        });     
      },
      ...
    });
  });

Note how we explicitly set the internalCurrency and externalCurrency. This is so that, if an object representing a particular currency has previously been instantiated and put into the identity map, then we can substitute that instance into the proposal rather than using the one that’s been provided by the object.

Where else should we use the identity map? Well, if you’ll recall from the data-structure diagram above, there’s also a bunch of objects representing monetary amounts. To support this, we have a Money mixin that we’re mixing into all monetary amounts. Here’s an example of it being used to decorate the cost of a MaterialCostItem:

angular.module('models').
  factory('MaterialCostItem', function(Base, Money) {
    return Base.extend({
      beforeMixingInto: function(object) {
        Money.mixInto(object.cost);
      }
    });
  });

The Money mixin in turn decorates the currency that is attached to the monetary amount:

angular.module('models').
  factory('Money', function(Currency, Base) {
    return Base.extend({
      beforeMixingInto: function(object) {
        Currency.mixInto(object.currency);
      },
      ...
    });

To identity map the currency, we would do the following:

angular.module('models').
  factory('Money', function(identityMap, Currency, Base) {
    return Base.extend({
      beforeMixingInto: function(object) {
        object.currency = identityMap('currency',
          Currency.mixInto(object.currency)
        );
      },
      ...
    });

By making this update in a single place, we’ll be identity-mapping the currency of all monetary amounts.

Finally, what if we want to identity-map currencies received directly from a server call (rather than those deserialized as part of a nested data structure)? Say that we had a service that got a list of all currencies from a back-end end-point called /currencies:

angular.module('services').
  factory('CurrencySvc', function(Restangular, Currency) {
    Restangular.extendModel('currencies', function(object) {
      return Currency.mixInto(object);
    });
 
    return Restangular.all('currencies');
  });

Fortunately, Restangular’s extendModel method lets us substitute in a completely different object if we want. So we can identity-map the currencies by simply altering it as follows:

angular.module('services').
  factory('CurrencySvc', function(
    Restangular, Currency, identityMap
  ) {
    Restangular.extendModel('currencies', function(object) {
      return identityMap('currency', Currency.mixInto(object));
    });
 
    return Restangular.all('currencies');
  });

Having put in all this identity-mapping of currencies, we can see it in action by tweaking the exchange rate of one of them, and then re-executing the calculations for a proposal – jump to the demo from my presentation at ng-conf to see it in action (unfortunately I can’t put all of the code online because it contains customer-sensitive information).

Caveats

Identity maps come with a couple of caveats that are always worth considering.

Most importantly, they leak memory, as they’re basically a hash of objects that can only increase in size the longer your app is sitting in the browser. Normally this is when somebody says “if you use an ES6 WeakMap to map class/ID pairs to object instances, those object instances will get garbage-collected if nothing else is referring to them”.

Unfortunately this is not the case as WeakMaps garbage-collect items if nothing is referring to the key, not the value. Whilst personally I mourn this missed opportunity, it’s worth noting that I haven’t had many issues in reality with identity maps causing memory blow-outs.

That said, you shouldn’t do identity-mapping just for its own sake, only when you need it. Identity maps effectively make object instances globally accessible, which introduces the possibility of unintended side-effects. Sometimes you don’t want there to be one global instance of an object – for example, if you’re editing instances but don’t yet want those changes to be reflected globally.

Wrapping Up

In this post I’ve shown how using rich object models with Angular opens up the possibility of employing identity mapping in your codebase. I’ve introduced a simple identity map implementation and demonstrated how it can be slotted into the mixin approach I described in my previous post. Identity maps have tradeoffs and shouldn’t be used indiscriminately, but are a handy tool to add to your toolbox for certain situations.

If you’re interested in more things you can do with Rich Object Models and Angular.js, check out the next post on Angular.js and Getter Methods.

Build vs Buy a Data Quality Solution: Which is Best for You? Maintaining high quality data is essential for operational efficiency, meaningful analytics and good long-term customer relationships. But, when dealing with multiple sources of data, data quality becomes complex, so you need to know when you should build a custom data quality tools effort over canned solutions. Download our whitepaper for more insights into a hybrid approach.

Topics:

Published at DZone with permission of Ben Teese, DZone MVB. See the original article here.

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}