Rich Object Models and Angular.js: Getter Methods

Rich Object Models and Angular.js: Getter Methods

Sadly not all getters can be giant robots

In my earlier posts on Rich Object Models and Angular.js and Angular Identity Maps I’ve shown how, by introducing a rich object model to your Angular.js application, you can begin to employ more sophisticated techniques for modelling your business domain.

In this post I’m going to demonstrate how we can simplify business logic by using getter methods to hide complex calculations behind properties. Furthermore, we’ll do it in a way that slots in nicely with our existing rich-object model. If you don’t feel like reading, you can check out the section of my ng-conf presentation that covers this or go straight to the Github project.

The Uniform Access Principle

So why would you want to hide a complex calculation behind a property? It comes down to a concept known as the Uniform Access Principle. This states that:

All services offered by a module should be available through a uniform notation, which does not betray whether they are implemented through storage or through computation

When performing complex calculations, adhering to this principle can be very helpful because you no longer have to worry about whether something is a property or the result of a calculation.

Bringing the Uniform Access Principle to your Javascript codebase is reasonably straightforward, as the ECMAscript 5.1 standard introduced the notion of getter methods. Getter methods provide a clear and concise notation for delegating a property lookup to a method. They are supported on just about every browser except Internet Explorer 8 and earlier.

A Javascript Example

Consider the code from my initial post that calculated the profits, revenues, etc for a proposal:

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

If we were to introduce getter methods for computed values, we’d change it as follows:

angular.module('models').
  factory('Proposal', function() {
    return {
      get profit() {
        return this.revenue.minus(this.cost);
      },
      get revenue() {
        return this.price.
          convertTo(this.internalCurrency);
      },
      get cost() {
        this.recurringEngineering.cost.plus(
          this.nonRecurringEngineering.cost
        );
      },
      ...
    };
  });

Much better. Getter methods are particularly useful when you are executing calculations that use a mixture of method invocations and property values, and don’t want to have to remember which are which.

In the proposal tool example, I could even have polymorphic properties – properties with the same name that were implemented in some mixins as methods and in other mixins as values – for example, a MaterialCostItem would have a cost property, whilst an InternalCostItem would have a cost method. By standardising on a property notion, I no longer had to worry about this distinction.

This in-turn makes refactoring easier. Continuing with the MaterialCostItem/InternalCostItem example, I found that in the NonRecurringEngineering mixin, I was repeatedly summing up costs across different collections of cost items:

angular.module('models').
  factory('NonRecurringEngineering', function(Base, Money) {
    return Base.extend({
      ...
      function materialCost() {
        var total = new Money();
        angular.forEach(this.materialCostItems,
          function(costItem) {
            total = total.add(costItem.cost);
          }
        );
        return totalMaterialCost;
      },
      function internalCost() {
        var total = new Money();
        angular.forEach(this.internalCostItems,
         function(costItem) {
           total = total.add(costItem.cost());
         }
        );
        return totalInternalCost;
      },
      ...
    });
  });

Obviously this is quite repetitive, but refactoring isn’t as straightforward as we’d hope because sometimes we’re totalling up property values and sometimes we’re totalling up the result of executing a method. One option is to check if the property is a function or not, and if it is, evaluate it:

angular.module('models').
  factory('NonRecurringEngineering', function(Base, Money) {
    function total(costItems) {
      var total = new Money();
      angular.forEach(costItems, function(costItem) {
        var cost = costItem.cost;
        if (angular.isFunction(cost)) {
          cost = cost.call(costItem);
        }
        total = total.add(cost);
      });
      return total;
    }

    return Base.extend({
      ..
      materialCost: function() {
        return total(this.materialCostItems);
      },
      internalCost: function() {
        return total(this.internalCostItems);
      },
      ...
    });
  });

But it’s a nuisance to have to do this whenever we want to refactor this sort of code. If instead we shift to getter methods, we can treat everything as a property, so things become easier to tidy up:

angular.module('models').
  factory('NonRecurringEngineering', function(Base, Money) {
    function total(costItems) {
      var total = new Money();
      angular.forEach(costItems, function(costItem) {
        total = total.add(costItem.cost);
      });
      return total;
    }
    ...
  });

A Minor Hiccup

Unfortunately getter-method support has one slight hitch: the angular.extend function – which is used under the hood by Base.extend and Base.mixin() – doesn’t copy getter methods into destination objects. Instead, it tries to get the actual property values, invoking the getter methods in the process. This is not an unreasonable thing to do, but it’s not what we want in this case.

To work around it I had to use my own extend implementation that deals specifically with the case that a property is defined with a getter method. This function is then used instead of angular.extend() in both Base.extend() and Base.mixin().

The implementation is a factory service called extend. It returns a single function and looks like this:

angular.module('shinetech.models', []).factory('extend',
  function() {
    return function extend(dst) {
      angular.forEach(arguments, function(src){
        if (src !== dst) {
          for (key in src) {
            var propertyDescriptor =
              Object.getOwnPropertyDescriptor(src, key);

            if (propertyDescriptor && propertyDescriptor.get) {
              Object.defineProperty(dst, key, {
                get: propertyDescriptor.get,
                enumerable: true,
                configurable: true
              });
            } else {
              dst[key] = src[key];
            }
          };
        }
      });

      return dst;
    };
  }
)

This implementation uses Javascript’s Object.getOwnPropertyDescriptor() method to determine whether a property on any of the source objects is defined using a getter method. If it is, then it uses Object.defineProperty() to setup a corresponding getter method on the destination object. If the property is not defined with a getter method, extend just does a regular copy of the property value.

Note that we provide a couple of special options when calling Object.defineProperty(). The first, enumerable, ensures that getter methods copied to the destination object will themselves be copyable later on. This scenario occurs when we extend an existing mixin (kind of like subclassing it), and then try to mix the resultant sub-mixin into an object. The second attribute, configurable, allows getter methods to be overridden by sub-mixins.

You can find the full, commented sourcecode in the Github project.

Wrapping Up

In this post I’ve talked about how the uniform access principle can be implemented in Javascript using getter methods. I’ve also introduced an implementation of extend that plays nicely with getter methods and thus allows us to use them with mixin-based rich object-models.

Getter methods can be used to cleanup calculation-intensive code where you don’t need to distinguish between methods and values. This in-turn reduces cognitive burden – for both you and others – when reasoning about your code.

Tags:
ben.teese@shinesolutions.com

I'm a Senior Consultant at Shine Solutions.

No Comments

Leave a Reply