7 Patterns to Refactor JavaScript Applications: Service Objects

  
Note: This is part two (prev/next) of a seven part series that starts here.

Service Objects are objects that perform a discrete operation or procedure. When a process becomes complex, hard to test, or touches more than one type of model, a Service Object is useful for cleaning up your code base. Even a Service Object that performs one single step can be a valuable abstraction for clarity and testing.

To ensure isolation of a Service Object, follow these principles:

  • Strict with input and output. Service Objects are designed to handle a very specific process, so we can forego the Robustness Principle in favor of creating a tool for a single, specific purpose.
  • Documented thoroughly. Since Service Object code likely lives in a different file than the code where it is used (e.g. a controller or model method), the reader will not have the benefit of context when trying to understand what the Service Object is doing. Hence, documenting the argument types, logical paths, and return values is crucial.
  • Terminates after completion. This pattern should not be conflated with a worker process, which could set an interval, listen for web socket messages, or perform some other procedure with no immediate end. Service Objects should be invoked, perform their operations (whether synchronous or asynchronous), and terminate.

Example

A program written for a teacher to grade their students has an end-of-year evaluation of whether or not the student passed the class. The process takes all assignments to be evaluated, finds the average of the percentage grades, and then assigns the grade to the student.

var _ = require('underscore');

/**
 * Determine whether a given student passes or not 
 * @constructor
 * @param {Object} student - the student
 */
var DetermineStudentPassingStatus = function(student) {
  this.student = student;
}

DetermineStudentPassingStatus.minimumPassingPercentage = 0.6

DetermineStudentPassingStatus.prototype = _.extend(DetermineStudentPassingStatus.prototype, {

  /**
   * The API method for calculating passing status from
   * a set of assignments that belong to the student
   * @param {Array} assignments - the student assignments
   */
  fromAssignments: function(assignments) {
    return _.compose(
      this.determinePassingStatus,
      this.averageAssignmentGrade,
      this.extractAssignmentGrades
    ).call(this, assignments);
  },

  // return the 'grade' value objects from each assignment
  extractAssignmentGrades: function(assignments) {
    return _.pluck(assignments, 'grade');
  },

  // take all grades and find the average percentage
  averageAssignmentGrade: function(grades) {
    return grades.reduce(function(memo, grade) {
      return memo + grade.percentage;
    }, 0) / grades.length;
  },

  // compare the averages from all assignments to the
  // minimumPassingPercentage value defined above
  determinePassingStatus: function(averageGrade) {
    return averageGrade >= DetermineStudentPassingStatus.minimumPassingPercentage;
  }

});

module.exports = DetermineStudentPassingStatus;

// new DetermineStudentPassingStatus(student).fromAssignments([assignments]);
// => true/false

By collecting this logic in an object, we provide a centralized location for adding hooks into the the process. For example, if an email needs to be sent to the parent when the student fails, we can add this to the object’s workflow with another method or another Service Object.

Testing

Even if the action grows in complexity, the test suite can still stay focused on this single procedure. Since each step is defined a method, prototype property, or constructor property, mocking out methods that interact with other parts of the application is easy in the test environment. This can help in preventing large test files and cumbersome environment preparation.

var expect = require('chai').expect;
var DetermineStudentPassingStatus = require('./determineStudentPassingStatus');
var Grade = require('./grade');

describe('DetermineStudentPassingStatus', function(){
  var student = {};
  var determineStudentPassingStatus = new DetermineStudentPassingStatus(student);

  describe('#fromAssignments', function(){
    var passing;

    it('returns true for passing grades', function(){
      passing = determineStudentPassingStatus.fromAssignments([
        {grade: new Grade(0.5)},
        {grade: new Grade(0.8)},
        {grade: new Grade(0.9)},
        {grade: new Grade(0.6)},
      ]);
      expect(passing).to.be.true;
    })
    
    it('returns false for failing grades', function(){
      passing = determineStudentPassingStatus.fromAssignments([
        {grade: new Grade(0.5)},
        {grade: new Grade(0.4)},
        {grade: new Grade(0.8)},
        {grade: new Grade(0.6)},
      ]);
      expect(passing).to.be.false;
    });
    
  });

});

Conclusion

Service Objects are a valuable tool for refactoring code. Isolating actions keeps logic clean, orderly, easy to test, and ultimately leads to a more maintainable and expressive code base. Remember, there’s no magic here—just a plain old JavaScript object that defines your procedure and executes it. Build them up however works for you!

In the next post, we’ll take a look at Form Objects, which can make form validation and persistence easy and context-specific.

The post was originally written for the Crush & Lovely Blog in 2012 and has been lovingly brought up-to-date for Engine Yard by Michael Phillips, the original author. Special thanks to Justin Reidy and Anisha Vasandani for help with the original.

Michael Phillips

Comments

Subscribe to our Blog