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

http://www.mariocasciaro.me/the-strange-world-of-node-js-design-patterns
  • Перевод
Здравствуйте, уважаемые читатели.

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

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.

  • +4
  • 22,3k
  • 5
Поделиться публикацией
Похожие публикации
Комментарии 5
  • 0
    Вопрос к тем кто проголовал за вариант «уже устарела», можете подробнее расрыть свою точку зрения?
    • +1
      Не голосовал, но могу предположить, что появление новых версий ноды, где появляются Промисы, импорты, генераторы, деструктуризация и прочие прелести ES2015 (aka ES6), делает устаревшими сведения о ранее принятых подходах вроде колбеков и require и т.п.
      • +1
        Можно предположить, что оценивают не книгу, а те кусочки текста, что выбраны для демонстрации:

        • Так как речь про ноду, то es6 уже во всю поддерживается, постепенно переход идет в эпоху es7, а книжка написана во времена es5

        • Приведенные тут примеры из книги по сути просто

          $ express myapp

          В котором уже есть более-менее ходовые актуальные паттерны для ноды, а более углубленные или специфичные лучше накладывать на es6 и более современные модули

        • Уже можно использовать нативные promise и даже вот-вот как async-await, а тут речь про сторонние библиотеки async и Q, которые проигрывает по производительности даже нативным промисам, не говоря уже о bluebird, который еще быстрее
      • +4
        Некоторые названия и термины лучше не переводить...
        • 0
          В заголовке:

          Перехват функций (латание по-обезьяньи плюс AOP)

          лучше бы не переводили Monkey Patching, если я правильно понял о чем этот заголовок. А то какой то блад ликинг для моих айзов получается.
          Вы же сами привели ссылку на вики где этого не стали делать… Ограничились "обезьяньим патчем". Никто же потом это Ваше обезьянье латанье не сможет загуглить.

          Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

          Самое читаемое