Статьи

Богатые объектные модели и Angular.js: методы получения

К сожалению, не все добытчики могут быть гигантскими роботами

В моих предыдущих публикациях о Rich Object Models и Angular.js и Angular Identity Maps я показал, как, внедрив богатую объектную модель в ваше приложение Angular.js, вы можете начать применять более сложные методы для моделирования вашей бизнес-области.

В этой статье я собираюсь продемонстрировать, как мы можем упростить бизнес-логику, используя методы получения, чтобы скрыть сложные вычисления за свойствами. Кроме того, мы сделаем это таким образом, чтобы оно вписывалось в нашу существующую модель rich-object. Если вам не хочется читать, вы можете ознакомиться с разделом моей презентации ng-conf, в котором это описано , или перейти непосредственно к проекту Github .

Принцип единого доступа

Так почему вы хотите скрыть сложный расчет за свойством? Это сводится к концепции, известной как принцип единого доступа . Это гласит, что:

Все услуги, предлагаемые модулем, должны быть доступны через единую систему обозначений, в которой не указано, реализованы ли они через хранилище или посредством вычислений.

При выполнении сложных вычислений соблюдение этого принципа может быть очень полезным, поскольку вам больше не нужно беспокоиться о том, является ли что-то свойством или результатом вычисления.

Внедрить принцип унифицированного доступа в вашу кодовую базу Javascript довольно просто, так как стандарт ECMAscript 5.1 ввел понятие методов получения . Методы получения обеспечивают четкую и краткую запись для делегирования поиска свойства методу. Они поддерживаются практически во всех браузерах, кроме Internet Explorer 8 и более ранних.

Пример Javascript

Рассмотрим код из моего
первоначального поста, который подсчитывал прибыль, доходы и т. Д. Для предложения:

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.