Mastering AngularJS Services Part 1 - Factories vs Services

This is part one of a two part series on how to effectively use the different service types that angular exposes to pass data cleanly around your application.

In this article we'll go in detail in to how to structure your application using services to keep your code organized and clean as it grows. In the next article, we will expand on the theory and example from part 1, learning how to use services to pass data between controllers and directives without polluting the scope or broadcasting events. You can go straight to communicating with directives by clicking here.

If you're like me, you learn best through example, so I'll start with some code outlining the main principles, and then go through creating a functioning example to apply the theory. A demo of what we will be creating can be found at the bottom of the page. I strongly encourage you to open the Plunkr up and edit the code to get a feel for it.


The Code

There are very subtle differences between factories and services, and getting it right can save you a lot of time debugging.

Factory

  1. Create an Object
  2. Add properties to it
  3. Return it

Person Factory

angular.module('FactoryServiceDemo').factory('Person', Person);

function Person () {  
  function PersonClass (firstname, lastname) {
    var self = this;

    // define properties
    this.firstname = firstname;
    this.lastname = lastname;

    // define methods
    this.getFullName = getFullName;

    /**
     * Return persons full name
     */
    function getFullName () {
      return self.firstname + ' ' + self.lastname;
    }
  }

  return (PersonClass);
}

Controller

angular.module('FactoryServiceDemo').controller('MainController', MainController);

function MainController ($scope, Person) {  
  $scope.person = new Person('John', 'Smith');
}

Service

  1. Add properties to this (the service will return this)
  2. Angular instantiates with new and returns the object

Directory Service

angular.module('FactoryServiceDemo').service('Directory', Directory);

function Directory (Person) {  
  var self = this;

  // array of Person objects
  this.people = [];

  // define methods
  this.addPerson = addPerson;

  /**
   * The addPerson method adds a person object to the people array
   */
  function addPerson (firstname, lastname) {
    self.people.push(new Person(firstname, lastname)); 
  }
}

Controller

angular.module('FactoryServiceDemo').controller('MainController', MainController);

function MainController ($scope, Directory) {  
  $scope.directory = Directory;
}

The Explanation

The core principle behind any "MVC" style framework is separation of concerns. Separation of concerns (SoC) is a design principle for separating an application into distinct sections, so that each section addresses a different and unique problem. The end goal is to create a modular application that is built up of individual, easily maintainable modules.

One of the most powerful tools in achieving this (when used correctly) are services. Services hold reusable application logic that can be consumed by controllers, directives and other services and help us keep our controllers thin.

Tip #1: Any business logic and persistent data in your app should be stored in a service.

Let's go through an example and register the different types of services to understand the advantages to each approach. We're going to create a very basic directory that holds details about different people. Too keep things simple, the only information we'll store about each person is their first and last names, however feel free to add more yourself.

We should be able to add and retrieve people to and from the directory, and easily expose the current data to any controllers or directives that request it.

The first step is to create a Person service to store each person's information. As the directory should hold the information of multiple people, we should be able to use the new syntax to create a new Person, so let's register this service as a factory.

/* Person.js */
angular.module('FactoryServiceDemo').factory('Person', Person);

function Person () {  
  function PersonClass (firstname, lastname) {
    var self = this;

    // define properties
    this.firstname = firstname;
    this.lastname = lastname;

    // define methods
    this.getFullName = getFullName;

    /**
     * Return persons full name
     */
    function getFullName () {
      return self.firstname + ' ' + self.lastname;
    }
  }

  return (PersonClass);
}

We create a PersonClass function and added two properties to it, firstname and lastname. We also added a method to return the persons full name, and then return it.

The next step is to create a Directory service to hold an array of Person objects. This service can also hold the logic to create, update, remove etc people from the directory. We want only one copy of this directory so that we can pass it around and get data wherever we need, so we will register this service as a service.

/* Directory.js */
angular.module('FactoryServiceDemo').service('Directory', Directory);

function Directory (Person) {  
  var self = this;

  // array of Person objects
  this.people = [];

  // define methods
  this.addPerson = addPerson;

  /**
   * The addPerson method adds a person object to the people array
   */
  function addPerson (firstname, lastname) {
    self.people.push(new Person(firstname, lastname)); 
  }
}

The differences between these two services are subtle but important. Instead of returning an object or function, we set the properties and methods to this. Within our controllers and directives, we can directly access all of these.

One thing to notice is var self = this;. This creates a private property that can be used internally by the directory service, but not from outside.

Finally, we can add a controller and a view to see these services in action.

/* MainController.js */
function MainController ($scope, Directory) {  
  var names = [ 'Vincent Chase', 'Johnny Drama', 'Ari Gold' ];

  $scope.directory = Directory.people;

  // loop through the names, adding them to the directory
  angular.forEach(names, function (name) {
    name = name.split(' ');
    Directory.addPerson(name[0], name[1]);
  });
}

In our controller we loop through an array of names, adding each one to the directory. We add the Directory.people array to scope so that we can use an ng-repeat in the view and output each persons full name.

<div ng-controller="MainController">  
  <table>
    <thead>
      <tr>
        <th>Directory</th>
      </tr>
    </thead>
    <tbody>
      <tr ng-repeat="person in directory track by $index">
        <td>{{person.getFullName()}}</td>
      </tr>
    </tbody>
  </table>
</div>  

The people array is passed by reference to the scope, therefore changes are reflected directly in the view. This means there is only ever one source of truth, maintained by the directory service throughout the life of the application.

You can see a working version including all the code in the Plunkr below. In the next article, we will expand on this example, adding more functionality, and introducing a directory list directive that can be reused throughout your application.


comments powered by Disqus