Node.js. Паттерны проектирования и разработки

в 10:15, , рубрики: design patterns, javascript, node.js, web-разработка, Блог компании Издательский дом «Питер», Веб-разработка, книги, проектирование, Профессиональная литература

Здравствуйте, уважаемые читатели.

Нас заинтересовала книга "Node.js Design Patterns", собравшая за год существования очень положительные отзывы, небанальная и содержательная.

Node.js. Паттерны проектирования и разработки - 1

Учитывая, что на российском рынке книги по 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 стимулирует разработчиков использовать определенные паттерны и повторяющиеся приемы. Мы рассмотрели некоторые из них и показали, как они позволяют эффективно решать распространенные проблемы, если применяются правильно. Кроме того, мы убедились, насколько по-разному может выглядеть паттерн в зависимости от реализации.

Автор: Издательский дом «Питер»

Источник

Поделиться новостью

* - обязательные к заполнению поля