Здравствуйте, уважаемые читатели.
Нас заинтересовала книга "Node.js Design Patterns", собравшая за год существования очень положительные отзывы, небанальная и содержательная.
Учитывая, что на российском рынке книги по Node.js можно буквально пересчитать по пальцам, предлагаем ознакомиться со статьей автора этой книги. В ней господин Кассиаро делает очень информативный экскурс в свою работу, а также объясняет тонкости самого феномена «паттерн» на материале Node.js.
Поучаствуйте, пожалуйста, в опросе.
Кроме классических паттернов проектирования, которые всем нам приходилось изучать и использовать на других платформах и в других языках, специалистам по Node.js то и дело приходится реализовывать в коде такие приемы и паттерны, которые обусловлены свойствами языка JavaScript и самой платформы
Преуведомление
Разумеется, паттерны проектирования, описанные бандой четырех, Gang of Four по-прежнему обязательны для создания правильной архитектуры. Но ни для кого не секрет, что в JavaScript нарушаются практически все правила, усвоенные нами в других языках. Паттерны проектирования – не исключение, будьте готовы, что в JavaScript придется переосмыслить старые правила и изобрести новые. Традиционные паттерны проектирования в JavaScript могут реализовываться с вариациями, причем обычные программерские уловки могут дорасти до статуса паттернов, поскольку они широко применимы, известны и эффективны. Кроме того, не удивляйтесь, что некоторые признанные антипаттерны широко применяются в JavaScript/Node.js (например, часто упускается из виду правильная инкапсуляция, так как получить ее сложно, и она зачастую может приводить к «объектному разврату», он же – антипаттерн «паблик Морозов».
Список
Далее следует краткий список распространенных паттернов проектирования, используемых в приложениях Node.js. Я не собираюсь вновь вам показывать, как реализуются на JavaScript «Наблюдатель» или «Одиночка», а хочу заострить внимание на характерных приемах, используемых в Node.js, которые можно обобщить под названием «паттерны проектирования».
Этот список я составил на материале из собственной практики, когда писал приложения Node.js и изучал код коллег, поэтому он не претендует ни на полноту, ни на окончательность. Любые дополнения приветствуются.
Причем я не удивлюсь, что вы уже встречали некоторые из этих паттернов или даже использовали их.
Требование директории (псевдо-плагины)
Этот паттерн – определенно один из самых популярных. Он заключается в том, чтобы потребовать все модули из директории, только и всего. При всей простоте это один из самых удобных и распространенных приемов. В Npm есть множество модулей, реализующих этот паттерн: хотя бы require-all, require-many, require-tree, require-namespace, require-dir, require-directory, require-fu.
В зависимости от способа использования требование директории можно трактовать как простую вспомогательную функцию или своеобразную систему плагинов, где зависимости не являются жестко закодированными в требующий модуль, а внедряются из содержимого директории.
Простой пример
var requireDir = require('require-all');
var routes = requireDir('./routes');
app.get('/', routes.home);
app.get('/register', routes.auth.register);
app.get('/login', routes.auth.login);
app.get('/logout', routes.auth.logout);
Более сложный пример (сниженная связность, расширяемость)
var requireFu = require('require-fu');
requireFu(__dirname + '/routes')(app);
Где каждая из /routes
– это функция, определяющая собственный url-маршрут:
module.exports = function(app) {
app.get("/about", function(req, res) {
// работаем
});
}
Во втором примере можно добавить новый маршрут, просто создав новый модуль, без необходимости изменять требующий модуль. Эта практика очевидно более мощная, кроме того, она уменьшает связность между требующими и требуемыми модулями.
Объект Приложение (самодельное внедрение зависимости)
Этот паттерн также очень распространен в других языках/на других платформах, но, в силу динамической природы JavaScript, этот паттерн оказывается очень эффективен (и популярен) в Node.js. В данном случае мы создаем один объект, который служит костяком всего приложения. Обычно этот объект инстанцируется на входе в приложение и служит клеем для различных прикладных сервисов. Я бы сказал, что он очень напоминает Фасад, но в Node.js он также широко применяется при реализации очень примитивного контейнера для внедрения зависимостей.
Типичный пример этого паттерна: в приложении есть объект App
(либо объект, одноименный самому приложению), и все сервисы после инициализации прикрепляются к этому большому объекту.
Пример
var app = new MyApp();
app.db = require('./db');
app.log = new require('./logger')();
app.express = require('express')();
app.i18n = require('./i18n').initialize();
app.models = require('./models')(app);
require('./routes')(app);
Затем App object
можно передавать по мере необходимости, чтобы им пользовались другие модули, либо он может принимать форму аргумента функции или require
Когда большинство зависимостей приложения прикреплены к этому стержневому объекту, зависимости фактически внедряются извне в те модули, которые его используют.
Но будьте внимательны: если пользоваться этим паттерном без обеспечения уровня абстракции над загруженными зависимостями, то у вас может получиться всезнающий объект, который сложно поддерживать и который, в принципе, по всем признакам напоминает антипаттерн God object.
К счастью, есть некоторые библиотеки, которые помогают справиться с этой проблемой – например, Broadway, архитектурный фреймворк, реализующий очень аккуратный вариант этого паттерна, обеспечивающий хорошую абстракцию и позволяющий лучше контролировать жизненный цикл сервиса.
Пример
var app = new broadway.App();
app.use(require("./plugins/helloworld"));
app.init(...);
app.hello("world");
// ./plugins/helloworld
exports.attach = function (options) {
// "this" – это наш объект приложения!
this.hello = function (world) {
console.log("Hello "+ world + ".");
};
};
Перехват функций (латание по-обезьяньи плюс AOP)
Перехват функций – еще один паттерн проектирования, типичный для динамических языков вроде JavaScript – как вы догадываетесь, он очень популярен и в Node.js. Он заключается в дополнении поведения функции (или метода) путем перехвата его (ее) выполнения. Обычно такой прием позволяет разработчику перехватить вызов до выполнения (prehook) или после (post hook). Тонкость заключается в том, что Node.js часто используется в комбинации с обезьяньим латанием, и эта техника оказывается очень мощной, но, в то же время, и опасной.
Пример
var hooks = require('hooks'),
Document = require('./path/to/some/document/constructor');
// Добавить методы перехвата: `hook`, `pre`и `post`
for (var k in hooks) {
Document[k] = hooks[k];
}
Document.prototype.save = function () {
// ...
};
// Определяем промежуточную функцию, которая будет вызываться после 'save'
Document.post('save', function createJob (next) {
this.sendToBackgroundQueue();
next();
});
Если вы когда-либо работали с Mongoose, то определенно видели этот паттерн в действии; если нет — в npm найдется масса подобных модулей на любой вкус. Но это еще не все: в сообществе Node.js термин «аспектно-ориентированное программирование» (AOP) зачастую считается синонимом перехвата функций, загляните в npm – и поймете, о чем я. Можно ли в самом деле называть это AOP? Мой ответ – НЕТ. AOP требует, чтобы мы применяли сквозную ответственность к срезу, а не прикрепляли вручную конкретное поведение к отдельной функции (или даже набору функций). С другой стороны, в гипотетическом AOP-решении на Node.js вполне могли бы применяться перехваты – тогда совет (advice) распространялся бы на множество функций, объединенных, к примеру, одним срезом, определяемым при помощи регулярного выражения. Все модули просматривались бы на соответствие этому выражению.
Конвейеры (промежуточный код)
Это суть Node.js. Конвейеры присутствуют здесь повсюду, отличаются по форме, назначению и вариантам использования. В принципе, конвейер — это ряд соединенных друг с другом обрабатывающих модулей, где вывод одного модуля служит вводом для другого. В Node.js это зачастую означает, что в программе будет ряд функций вида:
function(/* input/output */, next) {
next(/* err and/or output */)
}
Возможно, вы привыкли называть такие вещи промежуточным кодом (middleware) имея в виду Connect или Express, но границы использования данного паттерна гораздо шире. Например, Hooks – это популярная реализация перехватов (рассмотренных выше), объединяющая все pre/post функции в (промежуточный) конвейер, чтобы «обеспечить максимальную гибкость».
Как правило, этот паттерн реализуется тем или иным образом при помощи async.waterfall, или async.auto, или последовательности обещаний, причем может не просто управлять потоком выполнения, но и обеспечивать расширяемость той или иной части вашего приложения.
Пример: Async
async.waterfall([
function(callback){
callback(null, 'one', 'two');
},
function(arg1, arg2, callback){
callback(null, 'three');
}
]};
Черты конвейера есть и у другого популярного компонента Node.js. Как вы уже догадались, речь о так называемых потоках, а на что поток, если его нельзя конвейеризовать? Тогда как промежуточный код и цепочки функций вообще – универсальное решение для управления потоком выполнения и расширяемостью, потоки лучше подходят для обработки передаваемых данных в форме байтов или объектов.
Пример: потоки
fs.createReadStream("data.gz")
.pipe(zlib.createGunzip())
.pipe(through(function write(data) {
//... доводим данные до совершенства ...
this.queue(data);
})
// Записываем в файл
.pipe(fs.createWriteStream("out.txt"));
Выводы
Мы убедились, что по природе своей Node.js стимулирует разработчиков использовать определенные паттерны и повторяющиеся приемы. Мы рассмотрели некоторые из них и показали, как они позволяют эффективно решать распространенные проблемы, если применяются правильно. Кроме того, мы убедились, насколько по-разному может выглядеть паттерн в зависимости от реализации.
Автор: Издательский дом «Питер»