Страницы

суббота, 19 июля 2014 г.

Node.js Singleton

Небольшой этюд на тему шаблона singleton в контексте модулей Node.js. Как известно Node.js кэширует модули после первой загрузки, однако с кэшированием экспортируемых модулем объектов возможны варианты, в частности в случае экспорта конструктора объекта. Изложение пошаговое, в картинках. Результат - мой вариант решения вопроса. Заодно рассмотрим вопрос экспорта подключения к MongoDB.

Шаг 1. Экспортируем объект. Cоздаем приложение - app.js, а также импортируемый в него модуль - singleton.js:
app.js
var a;
for (var i = 1; i < 4; i += 1) {
  a = 'a' + i;
  module[a] = require('./singleton');
  console.log('counter: %d', module[a].counter);
}

singleton.js
var i = 0;
module.exports = {
  counter: ++i
};

Выполняем приложение:

Очевидно все три раза у нас один и тот же объект. Тот же результат получаем если экспортируем объект с помощью конструктора:
singleton.js
var i = 0;
module.exports = new Singleton();
function Singleton() {
  this.counter = ++i;
};

Шаг 2. Добавляем в конструктор аргумент:
app.js
var a;
for (var i = 1; i < 4; i += 1) {
  a = 'a' + i;
  module[a] = require('./singleton')({prop: i});
  console.log('prop: %d, counter: %d', module[a].prop, module[a].counter);
}

singleton.js
var i = 0;
module.exports = function(config) {
  return {
    prop: config.prop, 
    counter: ++i
  };
};

Выполняем:

В результате у нас три разных объекта, которые кстати можно создать и более традиционным способом:
app.js
var A, a;
for (var i = 1; i < 4; i += 1) {
  a = 'a' + i;
  A = require('./singleton');
  module[a] = new A({prop: i});
  console.log('prop: %d, counter: %d', module[a].prop, module[a].counter);
}

Как правило я использую иную конструкцию для экспорта конструктора:
singleton.js
var i = 0;
module.exports = function Singleton(config) {
  if (!(this instanceof Singleton)) return new Singleton(config);
  this.prop = config.prop;
  this.counter = ++i;
};

Импортируем модуль в приложение еще раз с одним из аргументов, переданных конструктору ранее:
app.js
var a;
for (var i = 1; i < 4; i += 1) {
  a = 'a' + i;
  module[a] = require('./singleton')({prop: i});
  console.log('prop: %d, counter: %d', module[a].prop, module[a].counter);
}
var b =  require('./singleton')({prop: 1});
console.log('prop: %d, counter: %d', b.prop, b.counter);


И в результате у нас четыре объекта, а хотелось бы три.

Шаг 3. Применяем шаблон singleton:
singleton.js
var i = 0;
var singleton = {};
module.exports = function Singleton(config) {
  if (singleton[config.prop]) return singleton[config.prop];
  if (!(this instanceof Singleton)) return singleton[config.prop] = new Singleton(config);   
  this.prop = config.prop;
  this.counter = ++i;
};

Вот теперь все в елочку:

Шаг 4. Применяем шаблон singleton для использования подключения к MongoDB. Создадим еще один модуль по имени db.js:
var Db     = require('mongodb').Db;
var Server = require('mongodb').Server;
var connection;

module.exports = function(cb) {
  if (connection) return cb(connection);  
  var db = new Db('test', new Server('127.0.0.1', 27017, { auto_reconnect: true }), {w: 'majority', safe: true});
  db.open(function(err, db) {
    if (err) throw err;
    connection = db;
    cb(db);
  });
};

Импортируем модуль в приложение:
app.js
require('./db')(function(db) {
  db.collection('test').findOne({}, function(err, item) {
    if (err) throw err;
    console.dir(item);
  });
});

Выполняем приложение и получаем документ коллекции test базы данных test:

Шаг 5. Импортируем модуль подключения к базе данных в модуль singleton:
singleton.js
var i = 0;
var singleton = {};
module.exports = function Singleton(config, cb) {
  if (singleton[config.prop]) return singleton[config.prop];
  if (!(this instanceof Singleton)) return singleton[config.prop] = new Singleton(config, cb);    
  this.prop = config.prop;
  this.counter = ++i;  
  require('./db')(function(db) {
    this.findOne = function(cb) {
      db.collection('test').findOne({}, {_id: 0}, cb);
    };
    cb();
  }.bind(this));
};

Вызываем модуль singleton в приложении три раза, два из которых с одинаковым аргументом:
app.js
var c = require('./singleton')({prop: 1}, function() {
  console.log('prop: %d, counter: %d', c.prop, c.counter);
  c.findOne(function(err, item) {
    if (err) throw err;
    console.log(item);
  });
});
var d = require('./singleton')({prop: 1}, function() {
  console.log('prop: %d, counter: %d', d.prop, d.counter);
  d.findOne(function(err, item) {
    if (err) throw err;
    console.log(item);
  });
});
var e = require('./singleton')({prop: 2}, function() {
  console.log('prop: %d, counter: %d', e.prop, e.counter);
  e.findOne(function(err, item) {
    if (err) throw err;
    console.log(item);
  });
});

Выполняем приложение:

Не смотря на то, что мы трижды создали объект, экспортируемый модулем singleton, в результате у нас в памяти два объекта, что и требовалось.

Шаг 6. Используем модули в веб-приложении.

Модуль app отправляет в модуль singleton в качастве аргумента имя коллекции:
app.js
var model = require('./singleton')({prop: 'test'}, function() {
  console.log('prop: %s, counter: %d', model.prop, model.counter);
  require('http').createServer(function (req, res) {
    model.findOne(function(err, item) {
      if (err) res.end(JSON.stringify({err: err.message}));      
      res.end(JSON.stringify(item));
    });    
  }).listen(1337, '127.0.0.1');
  console.log('Magic happens at http://127.0.0.1:1337/');
});

Модуль singleton асинхронно получает объект подключения к базе данных, после чего асинхронно же возвращает метод findOne, который в свою очередь использует полученное подключение для извлечения одного документа коллекции базы данныз test:
singleton.js
var singleton = {};
module.exports = function Singleton(config, cb) {
  if (singleton[config.prop]) return singleton[config.prop];
  if (!(this instanceof Singleton)) return singleton[config.prop] = new Singleton(config, cb);    
  this.prop = config.prop;
  this.counter = ++i;  
  require('./db')(function(db) {
    this.findOne = function(cb) {
      db.collection(this.prop).findOne({}, {_id: 0}, cb);
    };
    cb();
  }.bind(this));
};

Запускаем приложение:

Открываем URL приложения в браузере:

Вопрос решен. Есть иной, более вкусный вариант решения? Не стесняемся, делимся в комментах.