Предположим, мы разрабатываем простой пакет для npm. Назовём его, например,
storage
(хранилище). Заранее предусмотрим возможность использования одного из нескольких типов хранилищ, например, FsStorage
(файловое хранилище), MysqlStorage
(MySQL-хранилище), MongoStorage
(Mongo-хранилище).Накидаем содержимое исходных кодов нашего пакета (под спойлером):
package.json
:
{ "name": "storage", "version": "0.1.0", "main": "./lib/index.js", "dependencies": { "mysql": "*", "mongoose": "*" } }
lib/index.js
:
module.exports = { FsStorage: require('./fs-storage.jsx'), MysqlStorage: require('./mysql-storage.jsx'), MongoStorage: require('./mongo-storage.jsx') };
lib/fs-storage.js
:
var fs = require('fs'); module.exports = FsStorage; function FsStorage() { // init code... }
lib/mysql-storage.js
:
var mysql = require('mysql'); module.exports = MysqlStorage; function MysqlStorage() { // init code... }
lib/mongo-storage.js
:
var mongoose = require('mongoose'); module.exports = MongoStorage; function MongoStorage() { // init code... }
Непосредственный код зависимостей
mysql
и mongoose
для демонстрации нам необязателен. Поэтому, разместим вместо кода mysql
и mongoose
заглушки (под спойлером):node_modules/mysql/index.js
:
console.log('MySQL module loaded');
node_modules/mongoose/index.js
:
console.log('Mongoose module loaded');
Тогда файловая структура пакета будет выглядеть следующим образом (под спойлером):
storage/
├ lib/
│ ├ index.js
│ ├ fs-storage.js
│ ├ mongo-storage.js
│ └ mysql-storage.js
├ node_modules/
│ ├ mongoose/
│ │ └ index.js
│ └ mysql/
│ └ index.js
└ package.json
Теперь представим, что у нас есть мега-большая задача, которую будем выполнять с помощью нескольких дочерних процессов, и по ходу выполнения нам необходимо использовать хранилище в виде файлов.
var storage = require('storage');
var fsStorage = new storage.FsStorage();
Запускаем и наблюдаем: каждый дочерний процесс занимает памяти на порядок больше, чем ожидалось. А если количество конкурирующих процессов перевалит за сотню, и это не единственная задача, которая выполняется на сервере в реальном времени?!
Тут мы и понимаем, что, например, при использовании файлового хранилища, незачем загружать как библиотеку по управлению базами данных MySQL, так и ODM-клиент Mongo.
Сразу после вызова
require('storage')
в консоль выводятся сообщения:MySQL module loaded
Mongoose module loaded
Поэкспериментировав с методом
Object.defineProperty()
, я добился удивительного результата, который оформил в виде функции demandProperty()
:function demandProperty(obj, name, modPath) {
Object.defineProperty(obj, name, {
configurable: true,
enumerable: true,
get: function() {
var mod = require(modPath);
Object.defineProperty(obj, name, {
configurable: false,
enumerable: true,
value: mod
});
return mod
}
})
}
Принцип работы прост: Вместо прямой ссылки, например, на
MysqlStorage()
, создается акцессор (геттер). При любом запросе к акцессору, срабатывает require()
, а сам акцессор возвращает результат require()
. Кроме того, с помощью того же Object.defineProperty()
мы устанавливаем обратно прямую ссылку на тот же результат require()
вместо акцессора (то есть, на MysqlStorage()
). Так все запросы (разумеется, кроме первого) будут работать с той же скоростью и надежностью от утечек, как если бы мы оставили классический require()
.Изменим
lib/index.js
. Заменим:module.exports = {
FsStorage: require('./fs-storage.jsx'),
MysqlStorage: require('./mysql-storage.jsx'),
MongoStorage: require('./mongo-storage.jsx'),
};
на:demandProperty(module.exports, 'FsStorage', './fs-storage.jsx');
demandProperty(module.exports, 'MysqlStorage', './mysql-storage.jsx');
demandProperty(module.exports, 'MongoStorage', './mongo-storage.jsx');
И используем:
var storage = require('storage');
console.log(util.inspect(storage));
/* =>
{ FsStorage: [Getter],
MysqlStorage: [Getter],
MongoStorage: [Getter] }
*/
console.log(util.inspect(storage.FsStorage.name));
// => 'FsStorage'
console.log(util.inspect(storage));
/* =>
{ FsStorage: [Function: FsStorage],
MysqlStorage: [Getter],
MongoStorage: [Getter] }
*/
var mysql = new storage.MysqlStorage();
// => MySQL module loaded
console.log(util.inspect(mysql));
// => '{}'
console.log(util.inspect(storage));
/* =>
{ FsStorage: [Function: FsStorage],
MysqlStorage: [Function: MysqlStorage],
MongoStorage: [Getter] }
*/
Есть ещё одна тонкость. Если определение функции
demandProperty()
вынести за пределы модулей в папке lib
, последним аргументом необходимо передавать полный путь до модуля, иначе require()
будет искать модуль в той папке, где определен demandProperty()
:demandProperty(module.exports, 'FsStorage', path.resolve(__dirname, './fs-storage.jsx'));
demandProperty(module.exports, 'MysqlStorage', path.resolve(__dirname, './mysql-storage.jsx'));
demandProperty(module.exports, 'MongoStorage', path.resolve(__dirname, './mongo-storage.jsx'));
Всем удачных экспериментов!
Комментарии (20)
MarcusAurelius
02.06.2015 02:49+2Без
Object.defineProperty
эта задача прекрасно решалась, просто библиотека вместо хеша конструкторов должна возвращать функцию с одним параметром — именем провайдера. Это можно назвать фабрикой конструкторов. Фабрика делает нужныйrequire
и выглядит это гораздо лаконичнее.
Реализация модуля storage:
module.exports = function(providerName) { return require('./'+providerName+'-storage.jsx'); };
Вместо этого:
var storage = require('storage'); var fsStorage = new storage.FsStorage();
Загрузка модуля с провайдером выглядит так:
var storageConstructor = require('storage')('fs'), fsStorage = new storageConstructor();
ckr Автор
02.06.2015 06:46Вы правы, можно обойтись и без
Object.defineProperty()
.
Правда, я не использую в проектах фабрики из-за ненаглядности структуры кода, например, для IDE.
Так же мы используем общую документацию по всем проектам, пакетам, модулям. Для представления списка всех классов в древовидной форме, нам приходится лишь помогать jsdoc лишь указывая что есть@constructor
, а что@namespace
. Использование фабрик требовало бы модернизации сборщика/генератора документации.MarcusAurelius
02.06.2015 10:59А что, jsdoc хорошо понимает добавление геттеров через
Object.defineProperty
?ckr Автор
02.06.2015 11:13Нет, как раз за это отвечает метки
@constructor
,@namespace
и остальные.
Например, для обозначения namespace я добавлю вindex.js
:
/** @namespace storage */
А для класса, например:
/** * @name storage.FsStorage * @extends storage.BaseStorage * @constructor */
Классы я обычно определяю олдскул-способомFunction().prototype
.
Так, jsdoc шарится по всем файлам исходных кодов, а IDE тоже прекрасно понимает такие комментарии, структура кода всегда под рукой. Код готов к любому рефакторингу.MarcusAurelius
02.06.2015 11:19Вообще, меня jsdoc и IDE (все) очень огорчают. JavaScript не мыслим без примесей, замыканий и фабрик, а я вообще не могу жить без динамических примесей в прототипы. Они позволяют мне писать в 10 раз меньше кода. Пока не удалось найти систему комментирования исходников, генерации доков, или даже систему автодополнения кода для IDE, которая бы все это могла адекватно отпарсить и уразуметь. Ее конечно можно написать, но что-то подсказывает мне, что она будет очень. Обратное решение, когда все нужно декларировать явно, потребует много писанины в комментариях. Так что, единственный приличный способ сделать это, по моему мнению, это чтобы IDE подключались через дебагер к процессам и показывали содержимое прямо из памяти.
ckr Автор
02.06.2015 12:06Я не понимаю вашего негодования.
Я не против фабрик, но у каждого класса должно быть «жесткое» имя, закрепленное в своем namespace.
То есть, да, разумно к тому коду, что есть приделать возможность получения класса через фабрику. Это бы помогло в случае, например, если тип хранилища указан в конфиге. Но обязательно должна остаться возможность получить класс и по «жесткой» ссылке.
/** * @name storage.getStorage * @returns {storage.FsStorage|storage.MysqlStorage|storage.MongoStorage} */
Замыкания к процессу документирования вообще никакой гранью не могу отнести.
По поводу динамических примесей в прототипы. На Node.JS это реализованноutil.inherits()
.
А в jsdoc множественное наследование можно описать:
/** * @name storage.MysqlStorage * @extends {storage.BaseStorage|storage.SqlStorage} * @constructor */
MarcusAurelius
02.06.2015 13:02Где негодование? Нет признаков негодования.
Зачем нужна возможность именования классов по жесткой ссылке?
Замыкания относятся к документированию потому, что это не только способ написания прикладного кода, а еще и способ непрямого наследования. При использовании замыканий наследуются и переопределяются не классы и прототипы, а функции. Например каррирование — это функциональное наследование через замыкания. Например:
function curry(x) { return function(y) { return x + y; } }
Можем теперь делать: или сразуvar res = curry(4)(5);
или получать унаследованную функциюvar fn = curry(4);
и потом varvar res = fn(5);
С первого взгляда, может быть непонятно, зачем наследовать функции, но таким способом можно реализовывать метапрограммирование (порождающее программирование), см. примеры: habrahabr.ru/post/227753 Там замыкания повсеместно для порождения модификаций используются, и как это скормить в jsdoc? Это же все в рантайме создается.
Теперь проutil.inherits();
Это способ классового наследования. Это не имеет ни какого отношения к примесям в прототипы. Примеси в прототипы нужны когда мы хотим, опять же, динамически создать прототип для массового порождения объектов. Например, имеемClient.prototype
, и заранее мы не знаем, будет включен или отключен модуль безопасности. Когда он включен, то делаетсяrequire('security')(Client);
И уже сам модуль безопасности примешивает кClient.prototype
свои методы, например:Client.prototype.signIn(login, password);
и перекрывает своими методами часть базовых,Client
же не знал до этого что есть методы модуля безопасности, и он должен так расширитьClient
, чтобы новые методы объявить и вызывать их из уже имеющихся.ckr Автор
02.06.2015 13:59+1По части
curr
вы правы. Для корректного понимания IDE можно добавить в комментарий@returns {function}
. Ну, а как пользоваться сей конструкцией можно разъяснить в свободной форме в формате markdown в том же комментарии.
По поводу примисей и наследования я понял, что Вы хотели донести. Но все это можно реализовать и сutil.inherits()
. Разница лишь в том, чтоutil.inherits(Client, Security)
оставит методы и свойства Client, если таковые имеются в Security.
То есть, например, есть классы Security и Client. Если реализовать так, то методы и свойства Security будут поверх Client:
var SecurityClient = mixClass(Client, Security); function mixClass(cl, mx) { var resClass = function(){}; util.inherits(resClass, mx); util.inherits(resClass, cl); return resClass; }
Как оформить сие добро в jsdoc не могу уверенно предложить. Но, могу с уверенностью сказать, может примиси и удобный для web инструмент (в браузере много стандартных классов, в которых можно расширять функционал: HTMLDocument, XMLHttpRequest), но в Node.JS я бы уверенно обходился бы без них, с базовым одноуровневым наследованием.
Так или иначе, все зависит от конкретной задачи.ckr Автор
02.06.2015 14:12Заметил еще косяк, что потерял конструктор. Правильнее было бы так:
var SecurityClient = mixClass(Client, Security); function mixClass(cl, mx) { var resClass = cl.prototype.constructor; util.inherits(resClass, mx); util.inherits(resClass, cl); return resClass; }
ckr Автор
02.06.2015 14:32Даже возвращать новый класс необязательно, можно вкорячить прототип и в старый класс.
mixClass(Client, Security); function mixClass(cl, mx) { var resClass = cl.prototype.constructor; util.inherits(resClass, mx); util.inherits(resClass, cl); cl.prototype = Object.create(resClass.prototype); }
MarcusAurelius
02.06.2015 10:59Не использовать фабрики в js очень большие жертвы. Кстати, можно даже сделать фабрику фабрик, таким образом экранировать класс провайдера от модификации и избавиться от new.
Jabher
Lazy-load штука хорошая. Но я бы вам рекомендовал посмотреть на асинхронные задачи, все-таки в самой ноде обычно проводится на порядок меньше времени, чем в libuv.
К тому же клиент обычно не сразу инициализируется, а времени на загрузку тратится совсем немного.
У меня сейчас примерно так работает одна система, которая весьма бодро аггрегирует данные из соцсетей.
hell0w0rd
оО
await ожидает промис, а промис не может зарезолвить промис, таким образом что делает этот код?
Jabher
async-функции МОГУТ возвращать промисы как результат работы.
hell0w0rd
Интересно. Не является ли это специфичной для async/await «фичей», или багом regenerator?
hell0w0rd
таки нет, вы не правы.
Этот код вполне рабочий:
И да, все async-функции возвращают промис, как результат работы, by design.
Jabher
я имел ввиду, что await getPromise() способен асинхронно вернуть промис.
То, что с точки зрения архитектуры они все построены поверх промисов — я знаю.
hell0w0rd
Так зачем вы используете конструкцию await (await ...);?
ckr Автор
По началу, я тоже использовал одновременное асинхронное выполнение кода в несколько потоков. Но, код выполняется в одном процессе, соответственно, на одном ядре CPU. Для использования максимума ресурсов обычно используется оба подхода: код работает в несколько процессов, каждый процесс из которых выполняет код в несколько потоков.
Повышать количество потоков в каждом процессе может быть выгодно для экономии оперативной памяти, т.к. модули для второго и последующих потоков уже загруженны. Главное — не переборщить, во избежание зависания процессов. Но использование нескольких процессов тоже неизбежно.
ckr Автор
Кроме того, процессы node могут иметь такие состояния, когда их необходимо завершить или перезапустить: