Модель виджета служит для хранения отображаемых данных. В большинстве случаев для этих целей может использоваться обычный словарь вида:
var model = {
id: 5,
name: "Foo",
description: "Bar"
};
В сложных GUI этой простой структуры становится недостаточно и приходится решать следующие задачи:
- Где и как хранить логику изменения модели?
- Как получать данные модели от сервера?
- Как проверять валидность модели при ее инициализации и изменении?
Объект модели
Представление модели в виде словаря достаточно простое и, часто, достаточное решение. В случае необходимости, логику для изменения данных в этом словаре можно вынести в методы самого виджета или сторонние сервисы:
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, а полученные данные преобразовать в модель. В этом решении необходимо ответить на два вопроса:
- Кто должен отвечать за выполнение AJAX запроса?
- Кто должен отвечать за преобразование полученных от сервера данных в модель?
Первый вопрос решается двумя альтернативными подходами:
- Сама модель отвечает за запрос данных с сервера. В таком случае модель должна быть реализована в виде класса с дополнительной логикой, так как простого словаря будет не достаточно. Некоторым такое решение может не понравится, так как оно требует переноса логики, не относящийся к модели в классы этой модели.
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;
}
// Передача модели серверу
};