NodeJS красивый, модульный, объектный или делаем его таким с помощью redis и nohm

в 11:53, , рубрики: javascript, node.js, redis, игровой сервер, сервер, метки: , , ,

В последнее время в 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

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js