首页 > 代码库 > Angular model objects with JavaScript classes

Angular model objects with JavaScript classes

Angular model objects with JavaScript classes

The missing piece in AngularJS


Unlike Backbone and Ember, AngularJS does not provide a standardized way to define model objects. The model part of MVC* in Angular is the scope object, which is not what we mean by model objects. A model object is the JavaScript version of a class instance. Developers familiar with object oriented programming will feel right at home using model objects. I’m a big fan of the mix of OO and functional programming that JavaScript provides.

* Actually I think of AngularJS as a ViewModel-View-Controller framework, since $scope is better described as a ViewModel rather than a data Model.

Classes are blueprints for instantiating objects, which have a constructor and can have public, private and static properties as well as public, private and static methods. There are various ways in which you can define a class in JavaScript. I prefer using a named function, with methods added to the prototype. The main reason for this is the naming, which gives you the option to use the instanceof operator to check its type. Using the prototype has the advantage of sharing functions between objects of the same class, saving memory.

Whatever style of class definition you prefer, hooking them up to AngularJS is pretty straightforward. I prefer to use the factory function, but you can use the service or provider functions instead if you like. Here’s a complete model definition:

.factory(‘User‘, function (Organisation) {
 
  /**
   * Constructor, with class name
   */
  function User(firstName, lastName, role, organisation) {
    // Public properties, assigned to the instance (‘this‘)
    this.firstName = firstName;
    this.lastName = lastName;
    this.role = role;
    this.organisation = organisation;
  }
 
  /**
   * Public method, assigned to prototype
   */
  User.prototype.getFullName = function () {
    return this.firstName + ‘ ‘ + this.lastName;
  };
 
  /**
   * Private property
   */
  var possibleRoles = [‘admin‘, ‘editor‘, ‘guest‘];
 
  /**
   * Private function
   */
  function checkRole(role) {
    return possibleRoles.indexOf(role) !== -1;
  }
 
  /**
   * Static property
   * Using copy to prevent modifications to private property
   */
  User.possibleRoles = angular.copy(possibleRoles);
 
  /**
   * Static method, assigned to class
   * Instance (‘this‘) is not available in static context
   */
  User.build = function (data) {
    if (!checkRole(data.role)) {
      return;
    }
    return new User(
      data.first_name,
      data.last_name,
      data.role,
      Organisation.build(data.organisation) // another model
    );
  };
 
  /**
   * Return the constructor function
   */
  return User;
})

While the code and the comments explain a lot, there’s a few things to note here. Firstly, the name of the constructor (class name) is technically unrelated to the name of the Angular factory. You could give the factory and the constructor different names, but that will probably lead to a lot of confusion, so I recommend keeping these two in sync. In fact, minification would result in the class name to be changed to something short, while the factory name, being a string, remains the same. If you do choose to diverge here, remember that the factory name is what you instantiate and need for the instanceof check, while the class name is what you see when you log an instance to the console.

Secondly, wrapping classes in Angular factories will provide you with the option to use dependency injection. The model object itself can be injected elsewhere in your application, and you can inject other stuff into the factory. In the example I’ve injected Organisation, which is another class like this one. I strongly recommend to never inject anything but other models or filters in your models, otherwise you will end up with circular dependencies and all hell breaks loose. If you have a need to include services in your models, you’re doing it wrong. You can often avoid it by moving logic from the service into the model or a filter, or your model is trying to do too much already and logic should be moved into a service.

Using models with services

The most common place to create model objects is in services. I often find myself writing services for most of the models in my application. This is because models usually reflect a data resource (in RESTful terms), so it makes sense to have a service which acts on the API endpoint for this resource. For example, a basic OrganisationService:

.factory(‘OrganisationService‘, function (API, Organisation) {
  return {
    get: function () {
      return API
        .get(‘/organisations‘)
        .then(Organisation.apiResponseTransformer);
      });
    }
  };
});

The API service is just a wrapper for $http and returns a promise with the data of the response instead of the entire response (including headers and such). The most interesting to note here is the static methodOrganisation.apiResponseTransformer, which is passed as the callback function to then(). The goal is to have all of the organisations returned by the API to be mapped to the Organisation model. This allows you to verify the response data and enhance it with additional properties and methods. An apiResponseTransformer may look like this:

Organisation.apiResponseTransformer = function (responseData) {
  if (angular.isArray(responseData)) {
    return responseData
      .map(Organisation.build)
      .filter(Boolean);
  }
  return Organisation.build(responseData);
};

This code would be located in the class definition of Organisation. A nice thing about promises is that you can chain multiple calls to then(), so you can use several response transformers to handle complex response data (when dealing with a not-so-RESTful API for example). The result is still a promise, so it’s easy to use in your route resolve functions.

Extending classes

An important concept in object oriented programming is inheritance. Unlike true OO languages such as Java, JavaScript does not have class inheritance but uses prototypal inheritance. The main difference is that in JavaScript, you manually clone the prototype object from one instance to another (or link them by reference), instead of declaring inheritance on the class definition. There’s a few options you can explore:

/**
 * One-way reference from subclass to superclass (instance)
 * Most of the time this is what you want. It should be done
 * before adding other methods to Subclass.
 */
Subclass.prototype = new Superclass();
 
/**
 * Two-way reference
 * Superclass will also get any Subclass methods added later.
 */
Subclass.prototype = Superclass.prototype;
 
/**
 * Cloning behavior
 * This does not setup a reference, so instanceof will not work.
 */
angular.extend(Subclass.prototype, Superclass.prototype);
 
/**
 * Enhancing a single instance
 * This could be used to implement the decorator pattern.
 */
angular.extend(subclassInstance, SuperClass.prototype);

I’m not going to go into the details of implementing inheritance between model objects. There’s plenty of resources available online explaining the specifics. One reason is that I think this should be done sparingly, when it’s really the single best solution. Prototypal inheritance is a powerful thing, but it’s misunderstood by many. Chances are that using inheritance will only complicate things for you and your fellow developers, especially when they aren’t all JavaScript gurus. Most of the time it’s better to accept a little code duplication between models or move the logic into a filter or service. Also, you should prefer composition over inheritance.


Angular model objects with JavaScript classes