7 Patterns to Refactor JavaScript Applications: View Objects
When a model has associated logic or attributes that are used exclusively for creating a representation of the model—as in HTML for a website or JSON for an API endpoint—a best practice is to avoid storing those values directly on the model.
Storing view-specific attributes on the model can create confusion about what is truth (stored in the database) and what is purely representational. View Objects, then, act as an adapter between the truth and the representation of the truth.
For example, the truth of an imagined inventory item is that the price
attribute stored in the database is 599
cents. But the representation for the product page may be a mutation of the truth, such as $5.99
.
It would be inappropriate to store the representational data as a secondary price attribute on the model. It would be even worse to inject the formatting logic into the template.
What View Objects do is dress up the data, by way of transforming, adding, or removing data properties, returning a new object for use in the presentation layer. This approach creates a nice home for our presentation-specific logic and attributes, keeping it removed from the model truth.
Example
For example, at the end of the year, a teacher prints out report cards for each student. Among other information, the report card shows the student’s average grade, whether they are passing or not, and the phone number of the student.
The script to generate the report cards finds each student and their associated assignments for the year, producing a truthful representation of the object like this:
{
"id": "123456",
"firstName": "Susan",
"lastName": "Smith",
"gender": "f",
"phone": "5551234567",
"assignments": [
{
"grade": 0.65
},
{
"grade": 0.83
},
{
"grade": 0.90
},
...
]
}
The markup for the report card PDF, in keeping with the dogma of small views, is ignorant of the formatting of the data:
...
<p class="average-grade">Average grade across all assignments: </p>
<p class="passing-status">Passing: </p>
...
<p>
For any questions, please call the teacher at .
</p>
...
Presenting the student object for use in the view becomes very easy when we pass it into a View Object and in turn pass that View Object into the HTML. Here’s an example of how the View Object could be structured to dress our student model up for use in the HTML:
var _ = require('underscore');
var DetermineStudentPassingStatus = require('./determineStudentPassingStatus');
var GetAverageGradeFromAssignments = require('./getAverageGradeFromAssignments');
var GradeReportPresenter = function(student) {
this.student = student
}
GradeReportPresenter.prototype = _.extend(GradeReportPresenter.prototype, {
present: function() {
return _.compose(
this.averageGrade,
this.passingStatus,
this.phoneNumber
).call(this, {})
},
averageGrade: function(presentedStudent) {
// use a service object to determine average grade
var assignments = this.student.assignments
var averageGrade = new GetAverageGradeFromAssignments(assignments).run()
presentedStudent.averageGrade = averageGrade
return presentedStudent
},
passingStatus: function(presentedStudent) {
// use a service object to determine student passing status from assignments
var determinePassingStatus = new DetermineStudentPassingStatus(this.student.id)
var assignments = this.student.assignments
var passingStatus = determinePassingStatus.fromAssignments(assignments)
presentedStudent.isPassing = passingStatus
return presentedStudent
},
phoneNumber: function(presentedStudent) {
var phoneRegex = new RegExp(/(\d{3})(\d{3})(\d{4})/)
presentedStudent.phone = this.student.phone.replace(phoneRegex, "$1-$2-$3")
return presentedStudent
}
})
module.exports = GradeReportPresenter;
Testing
Unit testing for these objects is pleasantly straight-forward, since all you’re doing is passing in one object and expecting another. Given that, you can just test for the right properties and values after the data has been presented.
var expect = require('chai').expect
var GradeReportPresenter = require('./gradeReportPresenter')
var Grade = require('./grade')
describe('GradeReportPresenter', function(){
before(function(){
this.student = {
id: '123456',
firstName: 'Susan',
lastName: 'Smith',
gender: 'f',
phone: "5551234567",
assignments: [
{
grade: new Grade(0.65)
},
{
grade: new Grade(0.83)
},
{
grade: new Grade(0.90)
}
]
}
this.presented = new GradeReportPresenter(this.student).present()
})
it('returns only the specified properties', function(){
expect(this.presented).to.have.keys('phone', 'averageGrade', 'isPassing')
})
describe('#averageGrade', function(){
it('returns the correct value', function(){
expect(this.presented.averageGrade).to.equal(0.79)
})
})
describe('#isPassing', function(){
it('returns the correct value', function() {
expect( presented.isPassing ).to.equal(true)
})
})
describe('#phone', function(){
it('returns the correct value', function() {
expect(this.presented.phone).to.equal('555-123-4567')
})
})
})
Conclusion
View Objects are a very valuable tool for keeping your domain models small and as truthful (i.e. representative of the data in the database) as possible. They help you isolate presentation attributes from raw data attributes, and offer a greater level of flexibility, readability, and portability than using only domain models.
In the next post, we’ll take a look at Policy Objects, which provides a great tool for encapsulating business logic.
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