28 Feb 2014 Rich Object Models and Angular.js: Identity Maps
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:
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:
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.
David N. Johnson
Posted at 16:21h, 28 FebruaryUnless there are subtle complexities in this app that I’m not seeing, it looks like you’ve introduced another pattern purely to counteract the deficiencies of the mix-in that you used in the foundation article.
When I read through your code samples, all the boilerplate is setting off gigantic alarm bells in my head. Then there’s also the Identity Map itself which is basically just a gigantic global object store – when has that ever been a valid software practice? If you consider an alternate architecture where the currency conversion doesn’t occur on the model, but instead in a separate stateless service, then you no longer have the issue of propagating rate changes to every instance of currency.
Having said that, as per the previous post, the article itself is well written and the content is communicated clearly. Having never heard of Identity Maps before, I could now confidently implement one myself without issue so the article itself is good, it’s just I feel the primary focus should be shifted.
And now a personal rant –
Javascript isn’t a class-based language, it doesn’t have the language constructs that facilitate building complex models so I’m very suspicious when I see people try to emulate that behaviour. Prototypical inheritance is a poor replacement for classical OOP inheritance and that’s why the mix-in pattern has emerged to overcome this lack of ‘true’ inheritance and interfaces in functional languages.
When you’re using Ruby or Java, go for your life and build complex hierarchical models with rich behaviours and multiple inheritance to abstract away common methods. However, when you use Javascript, try to embrace composition and functional programming practices – use objects as data, mutate them through discrete functions that can be easily chained together and realise that you don’t need static models to represent the structure of your objects (because they’re not, objects are dynamic!).
There’s a growing segment of the Node.js community that really gets this, they follow the unix paradigm of piping data through lightweight, single-purpose programs and this turns out to be a model that really suits Javascript. Unfortunately, there’s a much larger portion of the community that are trying to apply the OOP model to Javascript – see Mongoose, which in my opinion is simultaneously the most popular and irrational database module to come out of Node. This sentiment applies to client-side javascript just as much as server-side.
Of course, this is highly opinionated and very argumentative so my views are based purely on my personal experiences working with Javascript. So hey, if you’re comfortable with classes and models then don’t let me try and stop you!
Ben Teese
Posted at 08:50h, 03 MarchInterested Onlookers: David is a colleague of mine at Shine. Initially he gave me this critique privately, but I thought it was so good that I got him to post it as a comment – whilst reserving my right to reply, of course 😉
@David:
I agree that there’s a bit of boilerplate. With some metaprogramming it could be reduced, I just haven’t got around to it and I don’t think it detracts too much from the broader concepts I’m trying to introduce.
Like I mentioned, identity maps are commonly used in object-relational mappers, and can also be found in a number of existing Javascript data-management libraries. Identity-maps are not something you’d want to use all the time, but I think that for some use-cases they are a valid solution. They can also be scoped to particular parts of your app, and flushed if you know you’ll no longer need the contents (for example, on logout).
I gave an example in my first post of what a stateless service for proposal calculations would look like that. It was bigger than the stateful mixin service, and it only represented the very top of a large tree of calculation. I believe that if you built stateless services for all of the business logic that the app required, that trend would remain.
Thanks!
I agree that for many scenarios, class-based object-orientation has been taken waaaaay too far (I’m looking at you, AbstractSingletonProxyFactoryBean) and functional solutions would more appropriate. That said, I don’t think we should throw the baby out with the bathwater and ditch object-orientation completely. For modelling real-world domain models – which I think the example in my posts is doing – I still think OO is a valid approach. For modelling more abstract concepts or doing complex data processing, not so much.
Grouping data and associated behaviour together (which is what object-oriented programming is) can be thought of as a refactoring strategy that, in some cases, removes duplication and reduces the number of function arguments you would have to pass around in a purely stateless system. The tradeoff is that you share state, which comes with its own risks. I think there’s room for both approaches in Javascript development.
Pingback:Rich Object Models and Angular.js | Shine Technologies
Posted at 10:34h, 04 March[…] approach sets up a clear path to more sophisticated object-oriented data modelling techniques like identity mapping, leveraging the uniform-access principle, method memoization and computed properties. If […]
Artem
Posted at 23:04h, 11 MayThank you for this article.
I have a question regarding this. If we have an angular service (which is a singleton) for each model, why not to introduce a solution which uses the local service scope as a storage of mapped objects rather than store them in a separate service? It would reduce the amount of boilerplate, eliminate the need to think about type/class names, and what’s more, it would allow to define what kind of logic should be used to describe an identity (an ‘id’ property or something else) in the model itself, since it is its responsibility to know things like that.