Паттерн «объект-запрос»
17 октября 2012 года Bryan Helmkamp, основатель Code Climate написал
пост описывающий 7 паттернов для рефакторинга толстых ActiveRecord
-моделей
в Ruby on Rails. Здесь, в Crush & Lovely, у всех Rails-разработчиков этот пост
является основным ориентиром для написания модульного, лаконичного, выразительного
и тестируемого кода.
В этой серии статей мы расскажем о подобных концепциях в JavaScript. Как и работа Bryan Helmkamp, эта серия так же применима к моделям данных, и не менее полезна. В этом посте обсудим паттерн «объект-форму.
Паттерны рефакторинга JavaScript-приложений
- Объект-значение
- Объект-сервис
- Объект-форма
- Объект-запрос
- Объект-представление
- Объект-политика
- Декораторы
Объект-запрос
Запросы к базам данных, даже самые простые, могут быть большими и сложными для чтения и понимания. Более сложные запросы, особенно те, которые взаимодействуют с несколькими коллекциями или таблицами, тяжело не только писать, но и поддерживать.
Паттерн «объект-запрос» обеспечивает хороший инструмент для отделения логики запросов и связанных с ними операций в отдельный модуль. Такое разделение позволяет получить горяздо более поддерживаемую и читаемую структуру, а так же обеспечивает очень понятный API для кода, использующего объект-запрос.
Пример
Давайте представим какой-нибудь метод API, который возвращает список всех студентов, перешедших на следующий курс, в JSON-формате.
Без использования объекта запросов у нас могла бы быть функция в API-контроллере или объект-сервис, как в примере ниже. Стоит отметить, что
DetermineStudentPassingStatus
я взял из статьи об объекте-сервисе.
// expecting all collection variables to be defined
var _ = require('underscore');
var Q = require('q');
var DetermineStudentPassingStatus = require('./determineStudentPassingStatus');
var getCurrentlyPassingStudents = function() {
var token = Q.defer();
// find all current students
studentCollection.findAll({ isCurrent: true }, function( students ) {
var studentIds = _( students ).pluck('_id');
// find all assignments for those current students
assignmentsCollection.findAll({ studentId: { $in: studentIds }}, function( assignments ) {
var passingStudentIds = [];
// group the assignments by studentId and then assess passing status
// adding the studentId to the array of passing students if passing
_( assignments ).chain()
.groupBy('studentId')
.each( function( assignments, studentId ) {
var passingStatus = new DetermineStudentPassingStatus( studentId ).run( assignments );
if ( passingStatus === true )
passingStudentIds.push( studentId );
})
.value();
// filter all current students down to those that are passing
// and resolve the deferred
var passingStudents = _( students ).filter( function( student ) {
return passingStudentIds.indexOf( student._id ) !== -1;
});
token.resolve( passingStudents );
})
})
return token.promise;
};
Мы сейчас не просто спустились на последние круги коллбек-ада, мы написали код, который очень сложно прочитать. Благодаря объекту запросов, мы можем создать гораздо более выразительный модуль.
var _ = require('underscore');
var async = require('async');
var Q = require('q');
var CurrentlyPassingStudentsQuery = function() {};
CurrentlyPassingStudentsQuery.prototype = _.extend( CurrentlyPassingStudentsQuery.prototype, {
run: function() {
this.deferred = Q.defer();
_.bindAll( this, 'fetchCurrentStudents', 'fetchAssignmentsForCurrentStudents', 'compilePassingStudentIds', 'filterAllPassingStudents', 'result' );
async.waterfall([
this.fetchCurrentStudents,
this.fetchAssignmentsForCurrentStudents,
this.filterAllPassingStudents
], this.result );
return this.deferred.promise;
},
fetchCurrentStudents: function( next ) {
studentCollection.findAll({ isCurrent: true }, function( currentStudents ) {
next( null, currentStudents );
});
},
fetchAssignmentsForCurrentStudents: function( currentStudents, next ) {
var currentStudentIds = _( currentStudents ).pluck('_id');
assignmentsCollection.findAll({ studentId: { $in: studentIds }}, function( assignments ) {
next( null, currentStudents, assignments );
});
},
compilePassingStudentIds: function( currentStudents, assignments, next ) {
var passingStudentIds = [];
_( assignments ).chain()
.groupBy('studentId')
.each( function( assignments, studentId ) {
var passingStatus = new DetermineStudentPassingStatus( studentId ).run( assignments );
if ( passingStatus === true )
passingStudentIds.push( studentId );
})
.value();
next( null, passingStudentIds );
},
filterAllPassingStudents: function( passingStudentIds, next ) {
var currentlyPassingStudents = _( students ).filter( function( student ) {
return passingStudentIds.indexOf( student._id ) !== -1;
});
next( null, currentlyPassingStudents );
},
result: function( err, currentlyPassingStudents ) {
if ( err ) {
this.deferred.reject( err );
} else {
this.deferred.resolve( currentlyPassingStudents );
}
}
})
Сгруппировав все связанные с запросом операции, мы получили более организованную структуру и создали выразительный API, который удобно использовать в приложении. Например, в виде контроллера ExpressJS:
var CurrentlyPassingStudentsQuery = require('./currentlyPassingStudentsQuery');
// for route GET /api/students/passing
var currentlyPassingStudents = function( req, res ) {
new CurrentlyPassingStudentsQuery().run()
.then(function( currentlyPassingStudents ) {
res.send( 200, currentlyPassingStudents );
})
.fail(function( err ) {
res.send( 422, err );
});
};
Данные, которые возвращает этот метод API будут простой строкой, полученной из хранилища данных, без какой-либо обработки. Зачастую, это не совсем то, что нам нужно. В этом примере, объект-запрос может быть объеденен с объектом-представлением (о котором мы напишем в следующей части). Объект-представление обеспечивает единое место для трансформации объекта перед показом пользователю.
Есть еще одна вещь, которую хочется отметить. Паттерн «объект-запрос»
открывает очень интересные возможности композиции. К примеру, в коде
приложения может быть много мест, где вам понадобится получить все оценки
определенной группы студентов, и в этом случае мы можем вынести эту
операцию в отдельный объект-запрос, and use it in the
#fetchAssignmentsForCurrentStudents
method.
Тестирование
Создание объекта-запроса вне контекста его использования позволяет легко покрывать его тестами. Если вы используете тестовую базу данных, все что вам нужно — загрузить необходимые данные в базу для того, чтобы обеспечить ожидаемые результаты при выполнении запросов. Остается только убедиться в том, что результаты корректные
var expect = require('chai').expect;
var CurrentlyPassingStudentsQuery = require('./currentlyPassingStudentsQuery');
describe('CurrentlyPassingStudentsQuery', function(){
var currentlyPassingStudents;
var err;
before(function( done ){
// first build all records in the necessary
// tables for testing (steps not shown)
// then run the Query Object
new CurrentlyPassingStudentsQuery().run()
.then( function( _currentlyPassingStudents ) {
currentlyPassingStudents = _currentlyPassingStudents;
done();
});
.fail( function( _err ) {
err = _err;
done();
});
});
it('returns the correct set of records', function(){
expect( currentlyPassingStudents ).to.have.length( expectedLength ); // however many you are expecting
});
});
В следующем посте мы обсудим объект-представление — отличный инструмент для изоляции преобразований данных модели в их представление.
Подписывайтесь на РСС. Всем добра!