Модель виджета служит для хранения отображаемых данных. В большинстве случаев для этих целей может использоваться обычный словарь вида:

var model = {
  id: 5,
  name: "Foo",
  description: "Bar"
};

В сложных GUI этой простой структуры становится недостаточно и приходится решать следующие задачи:

  1. Где и как хранить логику изменения модели?
  2. Как получать данные модели от сервера?
  3. Как проверять валидность модели при ее инициализации и изменении?

Объект модели

Представление модели в виде словаря достаточно простое и, часто, достаточное решение. В случае необходимости, логику для изменения данных в этом словаре можно вынести в методы самого виджета или сторонние сервисы:

function OrderWidget(model){
  this.model = model
}

OrderWidget.prototype.addOrderRow = function(row){
  this.model.row.push(row);
}

В этом примере виджет OrderWidget включает метод addOrderRow, который был вынесен из модели и служит для измененя ее состояния. Модель же представляется здесь простым словарем model и не содержит логики.

Плюсами данного решения являются:

  • Простота реализации

Минусами:

  • Запутанность логики, вызванная вынесением некоторых методов, относящихся к модели в виджет
  • Ориентация на процедурную парадигму программирования, лишающую разработчика выгод объектной-ориентации

Альтернативным решением является представление всех сущностей модели в виде отдельных классов:

function Order(client){
  this.client = client;
  this.rows = [];
}

Order.prototype.addRow = function(row){
  this.rows.push(row);
};

function OrderWidget(order){
  this.order = order;
}

Здесь логика изменения состояния модели является частью самой модели и вызывается непосредственно.

Плюсами данного решения являются:

  • Очевидность структуры модели
  • Объектная-ориентация, позволяющая, в том числе, использовать наследование моделей для их расширения и вынесения общих семантик

Минусами:

  • Сложность реализации, вызванная необходимостью дублировать модель бизнес-логики сервера на уровне представления

Для конкретной задачи необходимо выбирать ту реализацию, которая более приемлема и требует наименьших усилий. Так, для простого виджета вполне может подойти использование словаря в качестве модели, но сложный GUI лучше реализовывать с использованием классов модели.

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

Получение модели

AJAX

В Web-разработке модель виджета должна быть получена от сервера. Сделать это можно используя AJAX запрос к соответствующему методу API, а полученные данные преобразовать в модель. В этом решении необходимо ответить на два вопроса:

  1. Кто должен отвечать за выполнение AJAX запроса?
  2. Кто должен отвечать за преобразование полученных от сервера данных в модель?

Первый вопрос решается двумя альтернативными подходами:

  • Сама модель отвечает за запрос данных с сервера. В таком случае модель должна быть реализована в виде класса с дополнительной логикой, так как простого словаря будет не достаточно. Некоторым такое решение может не понравится, так как оно требует переноса логики, не относящийся к модели в классы этой модели.
function Model(){
}

// Метод для запроса данных модели с сервера
Model.prototype.get = function(){
  if(this.api === undefined){
    return false;
  }

  $.ajax(this.api, {
    dataType: 'json',
    success: $.proxy(function(data){
      // Логика преобразования данных сервера в модели представления
      this.client = data.client;
      this.rows = data.rows;
    }, this)
  });
};

function Order(client){
  // Вызов конструктора родителя
  Order.super.constructor.apply(this);

  this.client = client;
  this.rows = [];

  // Адрес метода API для запроса данных модели от сервера
  this.api = '/order/get';
}
// Наследование логики взаимодействия с сервером
Order.prototype = new Model;
Order.prototype.constructor = Order;
Order.super = Model.prototype;

// Другая логика модели
  • Сторонние сервисы отвечают за запрос данных с сервера. В таком случае модель освобождается от лишней для нее логики, но это усложняет структуру виджета и добавляет новые зависимости.
// Сервис для работы с Заказами
function OrderService(){
}

OrderService.prototype.get = function(callback){
  $.ajax('/order/get', {
    dataType: 'json',
    success: function(data){
      var order = new Order(data.client);
      order.rows = data.rows;

      callback(order);
    }
  });
};

Каждое решение имеет свои сильные и слабые стороны, но их лучше не комбинировать, а выбрать одну альтернативу, так как использование обоих решений в одном проекте только усложнит его структуру.

Второй вопрос (кто должен отвечать за преобразование полученных от сервера данных в модель) может быть решен множеством способов, но мы рассмотрим только два из них:

  • Модель расширяется методом, способным восстанавливать ее из “сырых” данных. Это довольно простое решение, но требующее некоторого усложнения структуры класса модели.
Order.prototype.init = function(data){
  this.client = data.client;
  this.rows = data.rows;
};
  • Используется сторонний сервис для восстановления модели.
OrderService.prototype.initModel = function(model, data){
  model.client = data.client;
  model.rows = data.rows;
}:

OrderService.prototype.get = function(callback){
  $.ajax('/order/get', {
    dataType: 'json',
    success: $.proxy(function(data){
      var order = new Order;
      this.initModel(data);

      callback(order);
    }, this)
  });
};

Hydration

Использование AJAX запросов для получения данных модели представления требует не только реализации API, но и часто сводит реализацию к Web-приложению. Другими словами, частью логики, отвечающая за отображение данных пользователю, полностью переносится в слой представления, а сервер лишается функций шаблонизации (это так же называется WebApp или одностраничным сайтом).

Если вы не готовы к таким изменениям, используйте метод hydration для восстановления данных модели прямо из HTML разметки. Делается это очень просто:

function OrderWidget(model, el){
  this.model = model;
  this.el = el;

  // Hydrate модели из документа
  if(this.el && !this.model){
    this.hydrate();
  }
}

OrderWidget.prototype.hydrate = function(){
  var client = $(this.el).find('input:hidden.client_id').val(),
    rows = [];

  $(this.el).find('input:hidden.row').each(function(){
    rows.push($(this).val());
  });

  this.model = new Order(client);
  this.model.rows = rows;
};

В данном случае метод hydrate вызывает виджетом только при указанном элементе HTML, из которого и должны быть восстановлены данные модели, а так же при отсутствии самой модели в конструкторе виджета. Метод hydrate пытается восстановить модель виджета из созданной ранее сервером HTML страницы.

Это решение не требует использования AJAX запросов, но может использоваться вместе с ним. Не пугайтесь, если вы увидите виджет, который умеет как запрашивать данные от сервера, так и восстанавливать их прямо из страницы.

Основным минусом данного решения является необходимость использования двух шаблонизаторов, как на стороне сервера, так и на стороне клиента. При этом если серверная логика реализуется не на том же языке программирования, что и клиентская, это приведет к дублированию шаблонов.

Верификация модели

После получения модели виджетом и при каждом изменении ее состояния необходимо проверить, не нарушены ли правила верификации. К примеру, для модели Order не допустимо оставлять свойство client незаполненным. Чтобы выполнить верификацию модели я предлагаю единственное, на мой взгляд, верное решение - реализовывать методы верификации прямо в модели и вызывать их при необходимости. Учтите, что методов верификации может быть больше одного, это актуально, если модель представления имеет множество состояний (о верификации объектов мы еще подробно поговорим в другой статье).

function Model(){
  // Ошибки верификации
  this.verifyErrors = [];
}

Model.prototype.isValid = function(){
  this.verify();
  // Успешно, если нет ошибок верификации
  return this.verifyErrors.length == 0;
};

function Order(client){
  this.client = client;
  this.rows = [];
}
// Наследование логики верификации
Order.prototype = new Model;
Order.prototype.constructor = Order;
Order.super = Model.prototype;

Order.prototype.verify = function(){
  // Добавление ошибки верификации, если клиент заказа не задан
  if(!this.client){
    this.verifyErrors.push('Не задан клиент заказа');
  }
  // Другая логика верификации
};

// Другие методы модели

Метод isValid должен вызываться разработчиком самостоятельно в те моменты, когда валидность модели представления наиболее критична. К примеру, этот метод можно вызвать перед передачей модели серверу для сохранения, чтобы убедиться, что она находится в пригодном для сохранения виде (конечно, требуется дополнительная верификация на сервере, но данное решение позволит отсеять часть невалидных запросов и предупредить пользователя об ошибках):

OrderService.prototype.save = function(order){
  // Завершение метода, если модель не валидна для передачи
  if(!order.isValid()){
    return false;
  }

  // Передача модели серверу
};