7 Patterns to Refactor JavaScript Applications: Policy Objects
Business logic always includes some boolean rules for data models, such as if a User is activated, or if a Student is passing. These rules interpret model attributes and return true
or false
, describing whether the object does or does not pass the policy.
For example, if a process sends an email to a given set of users, a Policy Object can be used to filter that set down to only those users that are email-able. A User would pass the Policy Object if they: have an email address, that email address has been verified, and they have not unsubscribed from correspondence.
Brian Helmkamp describes the potential overlap in concept between a Policy Object and a Query Object or a Service Object. He writes, “Policy Objects are similar to Service Objects, but I use the term ‘Service Object’ for write operations and ‘Policy Object’ for reads. They are also similar to Query Objects, but Query Objects focus on executing SQL to return a result set, whereas Policy Objects operate on domain models already loaded into memory.”
Example
Let’s imagine a collection of students that comprise the entire freshman student body. We want to filter out all students that are eligible to register for sports, which we define with these rules:
- Not suspended
- Not expelled
- Are passing all classes
Eligibility for sports enrollment isn’t a core concept of the Student
model and the logic is complex, so we can extract it into a Policy Object like this:
var _ = require('underscore');
var PassingStudentPolicy = require('./passingStudentPolicy');
var SportsEligibilityPolicy = function(student) {
this.student = student;
};
SportsEligibilityPolicy.prototype = _.extend(SportsEligibilityPolicy.prototype, {
run: function() {
return this.isNotExpelled() && this.isNotSuspended() && this.isPassing();
},
isNotExpelled: function() {
return this.student.isExpelled !== true;
},
isNotSuspended: function() {
return this.student.isSuspended !== true;
},
isPassing: function() {
return new PassingStudentPolicy(this.student).run();
}
});
module.exports = SportsEligibilityPolicy;
You can also see the composition opportunities for Policy Objects in the #isPassing
method, which uses another Policy Object to return whether or not the student is passing. Now policies that are part of our business logic are extracted from the model itself and put into unit-testable, easy to understand, and composable components.
Testing
Unit testing a Policy Object couldn’t be simpler: bbuild an object that should either pass or fail and ensure that the Policy Object returns the right boolean value.
var _ = require('underscore')
var expect = require('chai').expect;
var Policy = require('./Policy');
describe('Policy', function(){
it('returns true if the object passes the policy tests', function(){
var student = {
firstName: 'John',
lastName: 'Smith',
isExpelled: false,
isSuspended: false
};
var eligibility = new Policy(student).run();
expect(eligibility).to.be.true;
});
it('returns false if the student is expelled', function(){
var expelledStudent = {
firstName: 'John',
lastName: 'Smith',
isExpelled: true,
isSuspended: false
};
var eligibility = new Policy(expelledStudent).run();
expect(eligibility).to.be.false;
});
it('returns false if the student is suspended', function(){
var suspendedStudent = {
firstName: 'John',
lastName: 'Smith',
isExpelled: false,
isSuspended: true
};
var eligibility = new Policy(suspendedStudent).run();
expect(eligibility).to.be.false;
});
});
Conclusion
Even the simplest business rules can benefit—both from clarity and repetition—with extraction into a Policy Object. This pattern is incredibly simple to introduce and test, and can give big wins to the maintainability of your application.
In the next and final post of this series, we’ll take a look at Decorators, which are great for composing complex and varying processes.
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.
Share your thoughts with @engineyard on Twitter
OR
Talk about it on reddit