В этой публикации хочу поделиться одним из способов работы с данными. Началось все с того, что мне дали задачу сделать календарь, в котором каждая ячейка представляет собой некоторую форму, причем в зависимости от выбора пользователя. То есть, генерация шаблона ячейки должна была происходить на стороне клиента, в зависимости от выбора разных полей. Сложность заключалась в том, что поля каждой формы можно редактировать, а значит нужно где-то хранить все изменения.

От создания моделей на каждую форму я сразу отказался, поскольку при изменении одного поля, должно было каким-то образом меняться другое. Логика взаимодействия полей распространялась на 3 таблицы базы данных, из которых 2 хотя бы на половину заполнены данными и одна, в которую мне нужно передавать изменения. Поэтому я решил сделать собственное хранилище (как Store в Ext js). Суть заключалась в том, чтобы представить каждый узел объекта формата JSON в самостоятельную единицу. То есть принцип следующий: я получаю все 3 таблицы и создаю 3 дерева указателей, где каждый родитель является контекстом дочернего узла. Возможно, звучит, немного запутанно. Вот пример.

Заказчики
var Customers = [
  {
    "FirstName": "Customer first name 1",
    "LastName": "Customer second name 1",
    "Id": "1",
    "JobSpecificsList": [
      {
        "Id": "1",
        "Name": "Orders group 1"
      },
      {
        "Id": "2",
        "Name": "Orders group 2"
      }
    ],
    "WorkTypes": [
      "2",
      "0",
      "1"
    ],
    "PaymentTypes": [
      "1",
      "2",
      "3"
    ]
  },
  {
    "FirstName": "Customer first name 2",
    "LastName": "Customer second name 2",
    "Id": "2",
    "JobSpecificsList": [
      {
        "Id": "1",
        "Name": "Orders group 1"
      },
      {
        "Id": "61",
        "Name": "Orders group 3"
      },
      {
        "Id": "58",
        "Name": "Orders group 4"
      }
    ],
    "WorkTypes": [
      "2",
      "0"
    ],
    "PaymentTypes": [
      "1"
    ]
  }
]



Заказы
var orders = [
  {
    "Id":"1",
    "CustomerId": "1",
    "OrderName": "Order 1",
    "Start": "2015-04-12T11:22:00.0000000",
    "End": "2015-04-12T22:11:00.0000000"
  },
  {
    "Id":"2",
    "CustomerId": "2",
    "OrderName": "Order 2",
    "Start": "2015-04-12T11:22:00.0000000",
    "End": "2015-04-12T22:11:00.0000000"
  },
  {
    "Id":"3",
    "CustomerId": "1",
    "OrderName": "Order 3",
    "Start": "2015-04-12T11:22:00.0000000",
    "End": "2015-04-05T22:11:00.0000000"
  }
]



Расписание
var schedule = [
  {
    "CustomerId": "1",
    "StartWeek": "2015-04-13T00:00:00.000Z",
    "Schedule": [
      {
        "Id": "-1",
        "Start": "2015-04-15T17:00:00+06:00",
        "End": "2015-04-15T17:30:00+06:00",
        "WorkType": "0"
        "JobSpecificId": "-1",
        "PaymentType": "-1",
      },
      {
        "Id": "-1",
        "Start": "2015-04-16T19:00:00+06:00",
        "End": "2015-04-16T20:30:00+06:00",
        "WorkType": "2",
        "JobSpecificId": "-1",
        "PaymentType": "-1"
      },
      {
        "Id": "-1",
        "Start": "2015-04-19T00:00:00+06:00",
        "End": "2015-04-19T00:10:00+06:00",
        "WorkType": "-1",
        "JobSpecificId": "-1",
        "PaymentType": "-1"
      }
    ]
  },
  {
    "CustomerId": "2",
    "StartWeek": "2015-04-13T00:00:00.000Z",
    "Schedule": [
      {
        "Id": "-1",
        "Start": "2015-04-13T00:00:00+06:00",
        "End": "2015-04-13T00:00:00+06:00",
        "WorkType": "-1",
        "JobSpecificId": "-1",
        "PaymentType": "-1"
      }
    ]
  },
  {
    "CustomerId": "25",
    "StartWeek": "2015-04-13T00:00:00.000Z",
    "Schedule": [
      {
        "Id": "-1",
        "Start": "2015-04-19T00:00:00+06:00",
        "End": "2015-04-19T00:00:00+06:00",
        "WorkType": "-1",
        "JobSpecificId": "-1",
        "PaymentType": "-1"
      }
    ]
  }
]



Есть список заказчиков, список заказов и расписание отправки. В календаре происходит редактирование расписания. То есть курьер может просматривать назначенные на него заказы. В расписание так же можно добавить/удалить курьера. Добавление или удаление происходит понедельно.

Реализация


Объект Store отвечает за хранение, получение и отправку данных на сервер. Реализация примерно такая:

var Store = function(){
    var wrapSchedule= undefined;
    …
    this.loadScheduleList = function(){
        ...
        wrapSchedule = inMemory(serverResponse);
    };
    …
    this.getScheduleList = function(){ return wrapSchedule };
    …
    this.saveScheduleList = function(){
        var clientResponse= dumpAcc(wrapSchedule);
        ...
    };
}


Функция InMemory как раз занимается тем, что приводит объект или массив, полученный с сервера в дерево указателей:

Функция inMemory
   function inMemory(from) {
            return (function getNode() {
                var that = this;

                function facade(innerValue) {
                    function facadeArray(_innerValue) {
                        return (function(context) {
                            return {
                                get: function() {
                                    return context;
                                },
                                set: function(value) {
                                    context = clone(value);
                                },
                                indexOf: function(index) {
                                    return context[index];
                                },
                                push: function (v) {
                                    context.push(getNode.call(clone(v)));
                                },
                                splice: function(index, count) {
                                    context.splice(index, count);
                                },
                                orderBy: function (comparator) {
                                    context.sort(comparator);
                                },
                                filter: function (predicate) {
                                    return facadeArray(context.filter(predicate));
                                }
                            }
                        })(_innerValue);
                    }

                    function facadeObject(_innerValue) {
                        return (function(context) {
                            return {
                                get: function() {
                                    return context;
                                },
                                set: function(value) {
                                    context = clone(value);
                                },
                                update: function(obj) {
                                    var keys = Object.keys(obj);
                                    for (var prop_i = 0; prop_i < keys.length; prop_i++) {
                                        if (context[keys[prop_i]]) {
                                            context[keys[prop_i]] = clone(obj[keys[prop_i]]);
                                        }
                                    }
                                },
                                append: function(obj) {
                                    var keys = Object.keys(obj);
                                    for (var prop_i = 0; prop_i < keys.length; prop_i++) {
                                        context[keys[prop_i]] = clone(obj[keys[prop_i]]);
                                    }
                                }
                            }
                        })(_innerValue);
                    }

                    function facadePrimitive(_innerValue) {
                        return (function(context) {
                            return {
                                get: function() {
                                    return context;
                                },
                                set: function(value) {
                                    context = value;
                                }
                            }
                        })(_innerValue);
                    }

                    if (innerValue instanceof Array) {
                        return facadeArray(innerValue);
                    }

                    if (isPrimitive(innerValue)) {
                        return facadePrimitive(innerValue);
                    }

                    return facadeObject(innerValue);
                }

                if (that instanceof Array) {
                    for (var i = 0; i < that.length; i++) {
                        if (!isPrimitive(that[i]))
                            that[i] = getNode.call(that[i]);
                        else {
                            that[i] = facade(that[i]);
                        }
                    }
                }
                if (!(that instanceof Array)) {
                    for (var prop_i = 0; prop_i < Object.keys(that).length; prop_i++) {
                        var field = that[Object.keys(that)[prop_i]];
                        if (!isPrimitive(field)) {
                            that[Object.keys(that)[prop_i]] = getNode.call(field);
                        } else {
                            that[Object.keys(that)[prop_i]] = facade(field);
                        }
                    }
                }
                that = facade(that);
                return that;
            }).call(from);
    };


Доступ к каждому уровню осуществляется через get, set или любую другую функцию узла. Например в facadeArray можно реализовать метод first, тогда получение расписания определенного курьера за определенную неделю выглядело бы так:

var firstOnWeekPredicate = function(x){
    return x.get().CustomerId.get() == "1" && x.get().StartWeek.get() == '2015-04-13T00:00:00.000Z';
}

var scheduleByСourierIdAndWeek =  store.getScheduleList().first(firstOnWeekPredicate).get().Schedule

Для тех, кто еще не знает про такие удобные функции как where, first, last, union, map и т.д., советую посетить underscorejs.org

Теперь во внутреннее значение (массив) scheduleByСourierIdAndWeek можно добавить новый день недели или изменить существующий.

Соответственно:

scheduleByСourierIdAndWeek.push({
    "Id": "-1",
    "Start": "2015-04-15T00:00:00+06:00",
    "End": "2015-04-15T00:00:00+06:00",
    "WorkType": "0"
    "JobSpecificId": "-1",
    "PaymentType": "-1"
})

scheduleByСourierIdAndWeek.get()[0].update({WorkType: 2})

Весь «фасад» в функции getNode может быть переработан под собственные нужды.

Ну и что же мы получили в итоге? Мы разделили большой объект, полученный с сервера на связанные узлы. Раздали узлы в участки кода, где с ними происходит работа. Преобразовали данные так, как захотел пользователь. Теперь нужно все это собрать в объект и отправить обратно на сервер. Для этого есть функция dumpAcc.

   function dumpAcc(storeAcc) {
        return clone((function getNodeValue(acc) {
            if (typeof (acc.get ? acc.get() : acc) === "boolean"
                || typeof (acc.get ? acc.get() : acc) === "number"
                || typeof (acc.get ? acc.get() : acc) === "string") {
                return acc.get ? acc.get() : acc;
            }
            else if (acc.get() instanceof Array) {
                var _a = [];
                for (var _a_i = 0; _a_i < acc.get().length; _a_i++) {
                    _a.push(getNodeValue(acc.get()[_a_i]));
                }
                return _a;
            } else {
                var _o = {};
                for (var prop in acc.get()) {
                    if (acc.get().hasOwnProperty(prop))
                        _o[prop] = getNodeValue(acc.get()[prop]);
                }
                return _o;
            }
        })(storeAcc));
    }

То есть для того, чтобы получить clientResponse, нужно выполнить JSON.stringify(dumpAcc(store.getScheduleList())).
И напоследок функции clone, isPrimitive, которые я использовал.

    function clone(a) {
        return JSON.parse(JSON.stringify(a));
    }

    function isPrimitive(value) {
        return typeof (value) === "string" || typeof (value) === "number" || typeof (value) === "boolean";
    }

Комментарии (11)


  1. Aetet
    16.04.2015 11:47
    +1

    ну же, еще чуть-чуть и получится почти Flux.


    1. Karkat Автор
      16.04.2015 12:25
      -1

      Ни в коем случае! Я описывал только подход, примененный мной. А именно работу с полями объекта по ссылкам.


  1. Zenitchik
    16.04.2015 17:19

    Случалось. Работал. Подход прекрасен, но требует аккуратности.
    Причём, у меня ещё и узлы могли динамически создаваться, и ссылка должна была в нужную модель попадать.
    В юнит-тестах регулярно втыкал проверки ссылок на тождество =)


  1. pesh1983
    19.04.2015 09:28

    А почему не стали использовать модели и коллекцию из backbone? Например, при изменении конкретной модели можно подписаться на коллекцию, она проксирует события, а отправку моделей на сервер из коллекции можно делать и вручную, предварительно выбрав модели для отправки


    1. Karkat Автор
      19.04.2015 17:06

      Backbone как библиотека очень универсальна. Если бы я знал ее на должном уровне, то скорей всего использовал и потратил меньше времени.
      P.S.
      Насколько я понял flux и backbone используют в своей основе Mediator или Event Manager (напр. gist.github.com/howardr/118668). Я понимаю конечно, что он решает большинство всех проблем связывания. Но не кажется ли вам, что в данной ситуации это как-то притянуто за уши? Уж простите за мою нативность.


      1. pesh1983
        20.04.2015 07:50

        Я лишь хотел сказать, что вы реализовали то, что уже реализовано. Можно пойти и вашим путём, но, писать каждый раз свой велосипед может быть затратно по времени. На 99% потребностей, возникающих в процессе разработки, уже есть готовое решение, очень часто проверенное многими разработчиками в реальных проектах. Я, например, каждый раз корю себя за то, что начал писать свой велосипед, предварительно не поискав в интернете готовое ) Так ведь проще и быстрее.


  1. poluyanov
    20.04.2015 11:52

    Зачем вы оборачиваете все функции?

    return (function(context) {
         return {
           ...
         }
    })(_innerValue)
    
    


    1. Karkat Автор
      20.04.2015 14:30

      Это нужно для того чтобы сохранить контекст, к которому будут обращаться get/set. Т.е.

                                      get: function() {
                                          return context;
                                      },
                                      set: function(value) {
                                          context = clone(value);
                                      },
      

      это не будет работать, если вы не сделаете замыкание или не создадите объект.


      1. Karkat Автор
        20.04.2015 14:36
        -1

        Ну вот не понимаю я Хабрахабр. Вот за что минус? Может я что-то объяснил неправильно? Так напишите не стесняйтесь.


        1. poluyanov
          21.04.2015 10:18
          +1

          Ваши функции get и set и есть те самые замыкания. Соответственно так будет работать без лишних оберток ;)

          function facadePrimitive(context) {
               return {
                     get: function() {
                           return context;
                     }
          



          1. Karkat Автор
            21.04.2015 13:32

            Извините. Действительно бред. Забыл что в опубликованной реализации facade находится в getNode. Т.е. на каждый вызов getNode создается свой экземпляр facade.
            Сейчас я перенес функцию facade на один уровень с inMemory, поэтому и затупил). Спасибо за конструктивную подсказку.