Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
Ben Nadel at dev.Objective() 2015 (Bloomington, MN) with: Stacy London
Ben Nadel at dev.Objective() 2015 (Bloomington, MN) with: Stacy London@stacylondoner )

Breaking Direct Object References At Cache Boundaries In AngularJS

By Ben Nadel on

After I blogged about encapsulating the localStorage API in an AngularJS application, a couple of people asked me about why I was using angular.copy() when returning objects from the storage service. This is a great question and is not something that I've ever touched upon specifically. The goal of the angular.copy() usage is to maintain encapsulation around the implementation of the caching mechanism.


 
 
 

 
 
 
 
 

Run this demo in my JavaScript Demos project on GitHub.

I'm bringing this up in an AngularJS context because that's what the originating blog post was about. But really, the concepts of caching and encapsulation are universal. Encapsulation is generally concerned with implementation hiding. Having a well-hidden implementation is important because it means that you can change the internal implementation of a component without breaking the other components that depend on the refactored component.

In the context of caching, we need to break direct object references going into and coming out of the cache. This means that when someone sets an item, we have to store a copy of it. And, when someone retrieves an item, we have return a copy of it.


 
 
 

 
 Copy data at the cache boundaries in your AngularJS application. 
 
 
 

If we didn't perform these copy operations, mutations to an object outside of the cache may create unexpected side-effects inside of the cache. Essentially, we'd be side-stepping the cache's desire to manage its own data. While this might, at first, seem almost like a good thing - your cache is automatically kept in-sync - it ends up being extremely problematic. If your application starts to depend on this buggy behavior and then you switch to a different internal caching implementation like localStorage, SQLite, or IndexDB - all things that serialize data and break object references - your application will break despite the fact that your implementation is supposedly hidden from the rest of your components.

Luckily, performing a copy operation in AngularJS is quite easy; angular.copy() creates a deep copy of the source object and will handle both complex and simple data values. To see this in action, I've created a super simple cache service that exposes a .setItem() and a .getItem() method. Both of these methods perform a copy operation which safe-guards the cache against external mutations.

NOTE: angular.copy() will leave Function references in tact.

  • <!doctype html>
  • <html ng-app="Demo">
  • <head>
  • <meta charset="utf-8" />
  •  
  • <title>
  • Breaking Object References At Cache Boundaries In AngularJS
  • </title>
  • </head>
  • <body ng-controller="AppController">
  •  
  • <h1>
  • Breaking Object References At Cache Boundaries In AngularJS
  • </h1>
  •  
  • <!-- Load scripts. -->
  • <script type="text/javascript" src="../../vendor/angularjs/angular-1.4.2.min.js"></script>
  • <script type="text/javascript">
  •  
  • // Create an application module for our demo.
  • angular.module( "Demo", [] );
  •  
  •  
  • // --------------------------------------------------------------------------- //
  • // --------------------------------------------------------------------------- //
  •  
  •  
  • // I control the root of the application.
  • angular.module( "Demo" ).controller(
  • "AppController",
  • function provideAppController( $scope, cache ) {
  •  
  • var originObject = {
  • hello: "world"
  • };
  •  
  • // Store the origin object in the cache. This will store a copy of the
  • // object as it is configured at this moment in time.
  • cache.setItem( "test", originObject );
  •  
  • // TESTING: Since we cache a COPY of the object, we should be able to
  • // safely modify the origin object without corrupting the state of the
  • // cache.
  • originObject.foo = "bar";
  •  
  • // TESTING: Since the cache returns a COPY of the object, we should be
  • // able to safely modify the cached object without corrupting the state
  • // of the cache.
  • cache.getItem( "test" ).flippity = "floppities";
  •  
  • // Now that we've attempted to modify the object on each side of the
  • // caching workflow, let's pull it out of the cache again and verify that
  • // it has not changed. We're expecting to see an object with only one key.
  • console.log( "Cached Item:", cache.getItem( "test" ) );
  •  
  • }
  • );
  •  
  •  
  • // --------------------------------------------------------------------------- //
  • // --------------------------------------------------------------------------- //
  •  
  •  
  • // I provide a super simple key-value cache (overly simple for demonstration).
  • angular.module( "Demo" ).factory(
  • "cache",
  • function provideCache() {
  •  
  • // I hold the cached items in a key-value store.
  • var items = {};
  •  
  • // Return the public API.
  • return({
  • getItem: getItem,
  • setItem: setItem
  • });
  •  
  •  
  • // ---
  • // PUBLIC METHODS.
  • // ---
  •  
  •  
  • // I get the value stored at the given key.
  • function getItem( key ) {
  •  
  • // When we return an item from the cache, we want to return a copy
  • // of the cached item. If we were to return a reference to the cached
  • // object, the calling context could update the object remotely,
  • // thereby altering the state of the cache without going through the
  • // caching interface. That would be an unexpected behavior and would
  • // lead to confusing bugs.
  • return( angular.copy( items[ key ] ) );
  •  
  • }
  •  
  •  
  • // I store the given value at the given key.
  • function setItem( key, value ) {
  •  
  • // When we put an item into the cache, we want to store a copy of
  • // the item as it is currently. If we were to store the given object
  • // reference, the calling context could update the object remotely,
  • // thereby altering the state of the cache without going through the
  • // caching interface. That would be an unexpected behavior and would
  • // lead to confusing bugs.
  • items[ key ] = angular.copy( value );
  •  
  • }
  •  
  • }
  • );
  •  
  • </script>
  •  
  • </body>
  • </html>

As you can see, we attempt to mutate the cached object after both a .setItem() and a .getItem() call. However, since the angular.copy() operation breaks direct object references, our final call to .getItem() returns an object in the original state:

Cached Item: Object { hello="world" }

A copy operation is clearly not free, in terms of performance; but, I've never seen this have a palpable affect on performance in a real world application. For me, the safe-guard of consistent data is worth the minor overhead that is incurred by the copy operation.




Reader Comments

Running into this problem in our app was the strangest thing I'd ever seen; if I cache a reference client-side why on earth would I want operations on the object outside of the cache to modify something inside the cache?

It felt like something the caching library should handle instead of us. :-)

Reply to this Comment

Hi ben, Do you reckon to use same angular.copy approach before getting and setting values from cache if we are using a 3rd party modules like localStorageService or ngStorage etc?

Reply to this Comment

@Robert,

I was bit pretty hard by this in one of the InVision apps. The problem was that by the time I discovered the problem, the team had inadvertently built up other functionality that was dependent on it. Essentially, we had created a User-Experience that expected the cache data to be updated so that the next time live-data was received, it automatically maintained the current state of the View. When I went to try to untangle the mess, I realized I was going to have to change more and more code. Then, I started to be concerned that "fixing" the problem would accidentally start showing more bugs in other places (it was a shared Cache-factory kind of service). So, I ended up leaving the bug in place :(

Reply to this Comment

@Asad,

That's an interesting question. You definitely don't want to perform more copy-operations than you have to if you don't need to. One thing you could do is check to see if your particular 3rd-party library uses direct object references. And, if it does, you could create a library decorator that adds the copy operations, Example:

$provide.decorator( "myStorage", function( $delegate ) {

var originalSet = $delegate.setItem;
var originalGet = $delegate.getItem;

$delegate.setItem = function( key, value ) {
orginalSet.call( $delegate, key, angular.copy( value ) );
};

$delegate.getItem = function( key ) {
return( angular.copy( originalGet.call( $delegate, key ) ) );
};

return( $delegate );

} );

... or, you could create your own service object, like I did ("storage"), and then just move all that logic into the interaction between your service object and the 3rd-party service.

Reply to this Comment

Post A Comment

You — Get Out Of My Dreams, Get Into My Comments
Live in the Now
Oops!
Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.