- PVSM.RU - https://www.pvsm.ru -

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

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

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

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

Учитывая, что на российском рынке книги по Node.js можно буквально пересчитать по пальцам, предлагаем ознакомиться со статьей автора этой книги. В ней господин Кассиаро делает очень информативный экскурс в свою работу, а также объясняет тонкости самого феномена «паттерн» на материале Node.js.

Поучаствуйте, пожалуйста, в опросе.

Кроме классических паттернов проектирования [2], которые всем нам приходилось изучать и использовать на других платформах и в других языках, специалистам по Node.js то и дело приходится реализовывать в коде такие приемы и паттерны, которые обусловлены свойствами языка JavaScript и самой платформы

Преуведомление

Разумеется, паттерны проектирования, описанные бандой четырех, Gang of Four по-прежнему обязательны для создания правильной архитектуры. Но ни для кого не секрет, что в JavaScript нарушаются практически все правила, усвоенные нами в других языках. Паттерны проектирования – не исключение, будьте готовы, что в JavaScript придется переосмыслить старые правила и изобрести новые. Традиционные паттерны проектирования в JavaScript могут реализовываться с вариациями, причем обычные программерские уловки могут дорасти до статуса паттернов, поскольку они широко применимы, известны и эффективны. Кроме того, не удивляйтесь, что некоторые признанные антипаттерны [3] широко применяются в JavaScript/Node.js (например, часто упускается из виду правильная инкапсуляция [4], так как получить ее сложно, и она зачастую может приводить к «объектному разврату [5]», он же – антипаттерн «паблик Морозов».

Список

Далее следует краткий список распространенных паттернов проектирования, используемых в приложениях Node.js. Я не собираюсь вновь вам показывать, как реализуются на JavaScript «Наблюдатель [6]» или «Одиночка [7]», а хочу заострить внимание на характерных приемах, используемых в Node.js, которые можно обобщить под названием «паттерны проектирования».

Этот список я составил на материале из собственной практики, когда писал приложения Node.js и изучал код коллег, поэтому он не претендует ни на полноту, ни на окончательность. Любые дополнения приветствуются.
Причем я не удивлюсь, что вы уже встречали некоторые из этих паттернов или даже использовали их.

Требование директории (псевдо-плагины)

Этот паттерн – определенно один из самых популярных. Он заключается в том, чтобы потребовать все модули из директории, только и всего. При всей простоте это один из самых удобных и распространенных приемов. В Npm есть множество модулей, реализующих этот паттерн: хотя бы require-all [8], require-many [9], require-tree [10], require-namespace [11], require-dir [12], require-directory [13], require-fu [14].

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

Простой пример

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. В данном случае мы создаем один объект, который служит костяком всего приложения. Обычно этот объект инстанцируется на входе в приложение и служит клеем для различных прикладных сервисов. Я бы сказал, что он очень напоминает Фасад [15], но в 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 [16].

К счастью, есть некоторые библиотеки, которые помогают справиться с этой проблемой – например, Broadway [17], архитектурный фреймворк, реализующий очень аккуратный вариант этого паттерна, обеспечивающий хорошую абстракцию и позволяющий лучше контролировать жизненный цикл сервиса.

Пример

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 часто используется в комбинации с обезьяньим латанием [18], и эта техника оказывается очень мощной, но, в то же время, и опасной.

Пример

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 [19], то определенно видели этот паттерн в действии; если нет — в npm найдется масса подобных модулей [20] на любой вкус. Но это еще не все: в сообществе Node.js термин «аспектно-ориентированное программирование» (AOP) зачастую считается синонимом перехвата функций, загляните в npm [21] – и поймете, о чем я. Можно ли в самом деле называть это AOP? Мой ответ – НЕТ. AOP требует, чтобы мы применяли сквозную ответственность к срезу [22], а не прикрепляли вручную конкретное поведение к отдельной функции (или даже набору функций). С другой стороны, в гипотетическом AOP-решении на Node.js вполне могли бы применяться перехваты – тогда совет (advice) распространялся бы на множество функций, объединенных, к примеру, одним срезом, определяемым при помощи регулярного выражения. Все модули просматривались бы на соответствие этому выражению.

Конвейеры (промежуточный код)

Это суть Node.js. Конвейеры присутствуют здесь повсюду, отличаются по форме, назначению и вариантам использования. В принципе, конвейер — это ряд соединенных друг с другом обрабатывающих модулей, где вывод одного модуля служит вводом для другого. В Node.js это зачастую означает, что в программе будет ряд функций вида:

function(/* input/output */, next) {
    next(/* err and/or output */)
}

Возможно, вы привыкли называть такие вещи промежуточным кодом (middleware) имея в виду Connect [23] или Express [24], но границы использования данного паттерна гораздо шире. Например, Hooks [25] – это популярная реализация перехватов (рассмотренных выше), объединяющая все pre/post функции в (промежуточный) конвейер, чтобы «обеспечить максимальную гибкость».

Как правило, этот паттерн реализуется тем или иным образом при помощи async.waterfall [26], или async.auto [27], или последовательности обещаний [28], причем может не просто управлять потоком выполнения, но и обеспечивать расширяемость той или иной части вашего приложения.

Пример: Async

async.waterfall([
    function(callback){
        callback(null, 'one', 'two');
    },
    function(arg1, arg2, callback){
        callback(null, 'three');
    }
]};

Черты конвейера есть и у другого популярного компонента Node.js. Как вы уже догадались, речь о так называемых потоках [29], а на что поток, если его нельзя конвейеризовать [30]? Тогда как промежуточный код и цепочки функций вообще – универсальное решение для управления потоком выполнения и расширяемостью, потоки лучше подходят для обработки передаваемых данных в форме байтов или объектов.

Пример: потоки

fs.createReadStream("data.gz")
    .pipe(zlib.createGunzip())
    .pipe(through(function write(data) {
        //... доводим данные до совершенства ...
        this.queue(data);
    })
    // Записываем в файл
    .pipe(fs.createWriteStream("out.txt"));

Выводы

Мы убедились, что по природе своей Node.js стимулирует разработчиков использовать определенные паттерны и повторяющиеся приемы. Мы рассмотрели некоторые из них и показали, как они позволяют эффективно решать распространенные проблемы, если применяются правильно. Кроме того, мы убедились, насколько по-разному может выглядеть паттерн в зависимости от реализации.

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

Источник [31]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/javascript/113456

Ссылки в тексте:

[1] Node.js Design Patterns: http://www.amazon.com/Node-js-Design-Patterns-Mario-Casciaro/dp/1783287314/

[2] паттернов проектирования: http://www.piter.com/collection/starye-tovary/product/priemy-obektno-orientirovannogo-proektirovaniya

[3] антипаттерны: https://ru.wikipedia.org/wiki/%D0%90%D0%BD%D1%82%D0%B8%D0%BF%D0%B0%D1%82%D1%82%D0%B5%D1%80%D0%BD

[4] инкапсуляция: https://ru.wikipedia.org/wiki/%D0%A1%D0%BE%D0%BA%D1%80%D1%8B%D1%82%D0%B8%D0%B5_(%D0%BF%D1%80%D0%BE%D0%B3%D1%80%D0%B0%D0%BC%D0%BC%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D0%B5)

[5] объектному разврату: https://en.wikipedia.org/wiki/Object_orgy

[6] Наблюдатель: https://ru.wikipedia.org/wiki/%D0%9D%D0%B0%D0%B1%D0%BB%D1%8E%D0%B4%D0%B0%D1%82%D0%B5%D0%BB%D1%8C_(%D1%88%D0%B0%D0%B1%D0%BB%D0%BE%D0%BD_%D0%BF%D1%80%D0%BE%D0%B5%D0%BA%D1%82%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D1%8F)

[7] Одиночка: https://ru.wikipedia.org/wiki/%D0%9E%D0%B4%D0%B8%D0%BD%D0%BE%D1%87%D0%BA%D0%B0_(%D1%88%D0%B0%D0%B1%D0%BB%D0%BE%D0%BD_%D0%BF%D1%80%D0%BE%D0%B5%D0%BA%D1%82%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D1%8F)

[8] require-all: https://www.npmjs.com/package/require-all

[9] require-many: https://www.npmjs.com/package/require-many

[10] require-tree: https://www.npmjs.com/package/require-tree

[11] require-namespace: https://www.npmjs.com/package/require-namespace

[12] require-dir: https://www.npmjs.com/package/require-dir

[13] require-directory: https://www.npmjs.com/package/require-directory

[14] require-fu: https://www.npmjs.com/package/require-fu

[15] Фасад: https://ru.wikipedia.org/wiki/%D0%A4%D0%B0%D1%81%D0%B0%D0%B4_(%D1%88%D0%B0%D0%B1%D0%BB%D0%BE%D0%BD_%D0%BF%D1%80%D0%BE%D0%B5%D0%BA%D1%82%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D1%8F)

[16] God object: https://ru.wikipedia.org/wiki/%D0%91%D0%BE%D0%B6%D0%B5%D1%81%D1%82%D0%B2%D0%B5%D0%BD%D0%BD%D1%8B%D0%B9_%D0%BE%D0%B1%D1%8A%D0%B5%D0%BA%D1%82

[17] Broadway: https://github.com/indexzero/broadway

[18] обезьяньим латанием: https://ru.wikipedia.org/wiki/Monkey_patch

[19] Mongoose: https://github.com/Automattic/mongoose

[20] масса подобных модулей: https://www.npmjs.com/search?q=hooks

[21] npm: https://www.npmjs.com/search?q=aop

[22] срезу: https://en.wikipedia.org/wiki/Pointcut

[23] Connect: https://github.com/senchalabs/connect#readme

[24] Express: http://expressjs.com/

[25] Hooks: https://github.com/bnoguchi/hooks-js/

[26] async.waterfall: https://github.com/caolan/async#waterfall

[27] async.auto: https://github.com/caolan/async#auto

[28] последовательности обещаний: https://github.com/kriskowal/q#sequences

[29] потоках: https://nodejs.org/api/stream.html

[30] конвейеризовать: https://nodejs.org/api/stream.html#stream_readable_pipe_destination_options

[31] Источник: https://habrahabr.ru/post/278017/