В последнее время в IT-сообществе довольно много шумихи вокруг серверного JavaScript, в частности — NodeJS, однако, как это ни странно, оказалось довольно сложно найти информацию о том, как писать модульный, объектный код. Что я имею ввиду? Дело в том, что с js я знаком совсем недавно, до этого писал небольшие приложения на Java, а в свободное время пишу серверную часть онлайн-игры на PHP и, как и следовало ожидать, как и многим начинающим программистам на JS мне было очень непривычно вместо объектно-ориентированного использовать так называемое прототипно-ориентированное программирование. Тем более, JavaScript вносит достаточно много путаницы даже в это дело со своими Object.prototype и __proto__. Первое, что пришло мне в голову, как и многим другим разработчикам — сделать свою реализацию «привычного ООП», сделал. Немного подумав я решил, что это просто незачем, если мы работаем с nodeJS. За мою, хоть и недолгую практику, мне не довелось встретить задачу, которая бы требовала настоящего ОО подхода, я имею ввиду реальную необходимость наследования, полиморфизма и тем более инкапсуляции (конечно, все это нужно, но в той степени, которую js предоставляет).
Изучив довольно много приложений на nodeJS, я заметил, что почему-то практически нигде не используют паттерн MVC так, как это сейчас принято в большинстве PHP-фреймворков, хотя, эта модель мне кажется очень удобной и затраты на ее создание(как я думал в начале довольно серьезные) принесут свои плоды.
Реальный проект
Передо мной поставили задачу — реализовать сервер приложения «игровые автоматы» на node.js, казлось бы — довольно просто. Мне досталось довольно много кода от человека, который занимался этим раньше. В качестве БД используется Redis. Структура приложения выглядела примерно так:
-root
--application.js
--config.js
--constants.js
--import_data.js
--slots_module.js
--user_activity.js
--social.js
--modules/
---тут лежат модули для node.js
Довольно привычная для ноды структура, не так ли? :=)
Но с ростом требований к приложению, и количества всевозможного функционала, как и следовало ожидать его это приложение стало очень трудно поддерживать. Application и config разрослись на 5000 строк каждый, стало много дублирования кода и прочие прелести, стало практически невозможно определить где что находиться, не воспользовавшись поиском по проекту.
И вот, наконец, мы подошли к главному. Накипело. Я решил сделать капитальный рефакторинг и реорганизацию приложения. Первом делом, я вспомнил о том, что есть такая штука, как Object Relational Mapping(ORM) и к моему удивлению я нашел довольно неплохую реализацию ORM для NodeJS и Redis. Это послужило прекрасным толчком к использованию привычной мне архитектуры. Модуль nohm позволяет довольно просто описать модели, их свойства и методы, что позволяет сократить код, сделать его более структурированным и красивым. Вот простой пример того, описания и использование модели пользователя (User.js)
/**
* User: hlogeon
* Date: 31.07.13
* Time: 23:36
* TODO: continue creating this
* read http://maritz.github.io/nohm/
*/
var nohm = require('nohm').Nohm;
var redis = require('redis').createClient();
nohm.setClient(redis);
nohm.model('User', {
properties: {
balance: {
type: "integer",
defaultValue: 0,
index: false
},
ad_id: {
type: "string",
index: true
},
bonus_games_pending: {
type: "boolean",
index: false
},
chips: {
type: "integer",
defaultValue: 0
},
source: {
type: "string"
},
spins_count: {
type: "integer",
defaultValue: 0
},
mute: {
type: "boolean",
defaultValue: false
},
sound: {
type: "boolean",
defaultValue: false
},
charges_base: {
type: "boolean",
defaultValue: false
},
parent_ref: {
type: "string",
index: true
},
sid: {
type: "string",
index: true
},
bonus_to: {
type: "integer",
defaultValue: 0
},
points_count: {
type: "integer"
},
parent_id:{
type: "string",
index: true
},
invitation_accepted: {
type: "string"
},
ref_type: {
type: "string",
index: true
},
spins_temporary: {
type: "integer"
},
enter_date: {
type: "integer"
},
free_spins: {
type: "integer"
},
screen: {
type: "string"
},
last_game: {
type: "string"
},
startOffer: {
type: "boolean",
index: true
},
last_activity: {
type: "integer"
},
win_turn: {
type: "integer"
},
double_game_pending: {
type: "integer"
},
level: {
type: "integer",
index: true
},
last_spin: {
type: "integer"
},
uid: {
type: "string",
index: true
},
status: {
type: "string"
},
bonus_games_temporary: {
type: "integer",
defaultValue: 0
},
browser: {
type: "string"
},
builded: {
type: string,
}
},
methods: {
getContryFlag: function () {
return 'http://example.com/flag_'+this.p('country')+'.png';
},
updateBalance: function (value){
var balance = this.p('balance');
this.p('balance', balance+value);
this.save();
},
updateChips: function(value){
var chips = this.p("chips");
this.p("chips", chips+value);
this.save();
},
incrSpins: function(){
var spins = this.p('spins_count');
this.p('spins_count', spins+1);
this.save();
},
swichMute: function(){
var mute = this.p('mute');
this.p('mute', !mute);
this.save();
},
swichSound: function(){
var sound = this.p('sound');
this.p('sound', !sound);
this.save();
},
setPointsCount: function (value){
this.p('points_count', value);
this.save();
return value;
},
incrPointsCount: function(){
var count = this.p('points_count');
this.p('points_count', count+1);
this.save();
},
incrSpinsTmp: function(){
var tmp = this.p('spins_temporary');
this.p('spins_temporary', tmp+1);
this.save();
},
incrFreeSpins: function(){
var spins = this.p('free_spins');
this.p('free_spins', spins+1);
this.save();
},
incrLevel: function(){
var level = this.p('level');
this.p('level', level+1);
this.save();
return this.p('level');
}
}
});
var user = nohm.factory('User');
exports.user = user;
Пример использования:
var user = require('./UserTest').user;
app.get('/', function (req, res) {
var activeUser = nohm.factory('User');
activeUser.save(function(errs){
if(errs){
res.json(errs);
}
else{
res.json(activeUser.allProperties());
}
});
app.get('/findUser', function (req, res) {
var id = req.body.id;
user.load(id, function(err, aUser){
if(err){
res.json(err);
}
else{
res.json(aUser.allProperties);
}
})
});
Согласитесь, это куда проще постоянных redis.hgetall() тем более, что теперь мы можем определить методы пользователя в модели и даже Связи(Relations).
Благодаря такому подходу я разбил приложение на модули и новая структура выглядит так:
-root
--application.js
--constants.js
--config.js
--models/
---User.js
---Slotmachine.js
---Event.js
--helpers/
---Social.js
Хоть файлов стало и немного больше, зато, поддержка кода существенно упростилось, число строчек существенно снизилось, а читаемость возросла просто невероятно! Теперь мне не приходится распутывать лапшу из callback-функций 10 уровня вложенности. Все прозрачно, просто и понятно.
Надеюсь, кому-то будет полезна эта маленькая статейка от новичка в nodejs. Я буду очень благодарен за критику!
Автор: hlogeon