Недавно написал приложение с интерфейсом на JavaScript и Backbone.js. Закончилось всё тем, что при построении интерфейса у меня получилась трёхэтажная иерархия классов, с порядка 10 конечными дочерними классами. Пришла пара идей, как лучше справляться с такими иерархиями, о чём и расскажу.
Навороченные интерфейсы на JS только недавно вошли в моду, в то время как принципы ООП уже совсем не новы. Веб-приложения становятся мало отличимыми от обычных приложений. Так что пришло время соединять моду с классикой.
Теорию интереснее всего рассматривать на примерах.. каким веб-приложением вам хотелось бы пользоваться?
Встал утром - и за 10 минут проверил всё, что требует моего внимания, на одной странице.
Пример, конечно, немного надуманный, т.к. подсоединять всевозможные сервисы к dashboard при отсутствии API сложно и нецелесообразно. Вместо такого dashboard'а я использую, в основном, почту и закладки в браузере.
Что общего у виджетов?
Чем отличаются наши виджеты друг от друга?
Пример с третьей разновидностью виджета говорит о том, что мы не всегда знаем, какой система будет завтра. Можно было бы сразу реализовать всё через websocket'ы, но предположим, что мы уже реализовали (1), а про вебсокеты вспомнили через месяц, когда уже накопилось с десяток виджетов типа (1) и (2), и они уже выстроились в некую иерархию... и добавление функционала потребует изменений иерархии. А именно, нам придётся подсоединиться к вебсокетам в методе initialize():
А также мы можем захотеть добавлять новые элементы управления. Тогда нам придётся переопределить метод render():
Вернёмся от частности к общему. Мы хотим расширять приложение. При этом мы не знаем, какой функционал понадобится завтра. Раз уж мы пишем на backbone.js, в классе нового виджета нам нужно переопределить 2 метода - render() и initialize(), и изменять придётся очень по-разному.
Иерархия классов может стать слишком запутанной.
С каждым наследованием функции render() и initialize() оборачивается в соответствующие функции дочернего класса. Дополнительное неудобство в том, что если нужно скомпоновать или обработать данные перед отрисовкой, нам придётся как-то передавать их в render() базового класса. Т.е. придётся добавлять аргументы, что выглядит некрасиво:
Получившиеся связи методов показаны синим и зелёным:
Мы слишком увлеклись. Мы ведь не обязаны переопределять методы render() и initialize() в каждом дочернем классе. Единственное что мы должны - это изменить их в дочернем классе.
Самое время вспомнить паттерн «шаблонный метод».
Оставим входные аргументы render() в покое. Не будем переопределять методы. Если нам нужно изменить данные - добавим хук для получения кастомных данных:
Если же нужно добавить новые элементы управления - добавим хук для создания этих элементов управления:
Получится иерархия без запутанных связей:
Конечно же, в случае сложной иерархии, мы можем захотеть переопределять хуки в дочерних классах, чтобы их расширить, или даже добавлять хуки в хуки. Но с этим лучше не слишком усердствовать, особенно если будущее приложения до конца не ясно. Преимущество в том, что сейчас мы можем сделать всё в виде плоской структуры.
Самое интересное: мы можем не думать о дочерних классах, когда создаём базовый. Просто создавая новый вид виджета, добавим нужные ему хуки в базовый класс. Какой бы ни была глубина иерархии, мы можем добавить хук со своим именем в базовый класс, специально для конкретного дочернего класса - тогда не придётся вообще ничего переопределять!
В классической реализации паттерна «шаблонный метод» обычно предполагается, что базовый алгоритм уже определён. Однако, мы можем дополнять базовый алгоритм новыми хуками только для классов, которым эти хуки нужны. Для остальных - они будут пропускаться.
Полезности применения такого подхода:
Навороченные интерфейсы на JS только недавно вошли в моду, в то время как принципы ООП уже совсем не новы. Веб-приложения становятся мало отличимыми от обычных приложений. Так что пришло время соединять моду с классикой.
Теорию интереснее всего рассматривать на примерах.. каким веб-приложением вам хотелось бы пользоваться?
Немного фантазии
Мне хотелось бы иметь универсальный dashboard, то есть рабочую панель на все случаи жизни. Она будет показывать статистику интересующих меня сообществ, информацию по серверам и сайтам, которыми я управляю, погоду на ближайшие выходные, тщательно отфильтрованные новости. Также там будут некоторые элементы управления.Встал утром - и за 10 минут проверил всё, что требует моего внимания, на одной странице.
Пример, конечно, немного надуманный, т.к. подсоединять всевозможные сервисы к dashboard при отсутствии API сложно и нецелесообразно. Вместо такого dashboard'а я использую, в основном, почту и закладки в браузере.
Реализация dashboard
Наш dashboard будет просто «доской», заполненной виджетами. А в каждом виджете может быть что угодно:
Каждый виджет - это объект. Я использую backbone.js для построения интерфейсов, который мы рассматривать подробно не будем, зато мы рассмотрим простой ООП-паттерн на примере отображения объекта.. Backbone.js даёт нам базовый класс Backbone.View, от которого мы и будем наследовать каждый виджет. Метод, отображающий виджет, в Backbone принято называть render():
(Целиком, простейшее приложение на backbone.js можно посмотреть тут)
Считаем, что мы уже написали приложение, которое получает данные с сервера и отображает один виджет по этим данным:
Но мы хотим множество виджетов, с разным функционалом!
- Они привязаны к соответствующим моделям Backbone, с отображаемыми данными.
- Их можно перемещать, менять размер, скрывать, удалять. Это значит, что в методе render() мы добавим общие для всех элементы управления.
То, что есть общий функционал, уже требует, чтобы мы создали базовый класс виджета:
App.views.Widget = Backbone.View.extend({
initialize: function(){
// перерисовка при изменении модели
this.model.listenTo(this.model, 'change', this.render);
},
render: function(){
// Отображаем виджет в DOM, используя данные модели:
this.$el.html(this.template({model: this.model.attributes}));
// Добавляем элементы управления:
...
},
});
- Одна разновидность просто отображает данные: какой-нибудь график, или картинку погоды.
- Через другой тип виджетов мы можем быстро изменить какое-то значение. Например, поменять статус в социальных сетях. Удобнее всего, когда мы заполнили поле, и оно сохранилось автоматически. Полей может быть несколько - например мы можем захотеть менять «товар дня» в своём интернет-магазине, и назначать ему скидку. Всё это отобразится на сайте немедленно (это сделает серверная часть, которую мы не рассматриваем).
- Третья разновидность обновляется в реальном времени - например лента Твиттера, сообщения из разных соц. сетей, или мониторинг форумов. Совсем забыл: это примерно то же, что и первая разновидность. Но первая обновляется, скажем, раз в час. Это можно делать по таймеру. А для реального времени нам нужно держать постоянное соединение с сервером - используя websocket.
App.views.TweetsWidget = App.views.Widget.extend({
initialize: function(){
// Инициализация базового класса:
App.views.Widget.prototype.initialize.apply(this, arguments);
// Подписываемся на событие от вебсокета:
App.websocket.on('graph-update', _.bind(function(){
// обновляем Backbone-модель, содержащую данные виджета:
this.fetch();
}, this));
},
});
А также мы можем захотеть добавлять новые элементы управления. Тогда нам придётся переопределить метод render():
App.views.TweetsWidget = App.views.Widget.extend({
initialize: function(){
...
},
render: function(){
// Отрисовка базового класса:
App.views.Widget.prototype.render.apply(this, arguments);
// Добавление новых элементов управления:
...
},
});
Вернёмся от частности к общему. Мы хотим расширять приложение. При этом мы не знаем, какой функционал понадобится завтра. Раз уж мы пишем на backbone.js, в классе нового виджета нам нужно переопределить 2 метода - render() и initialize(), и изменять придётся очень по-разному.
Иерархия классов может стать слишком запутанной.
- На 1-й итерации разработки мы реализуем базовый класс виджета, и несколько дочерних классов простых графиков.
- На 2-й итерации - сделаем редактируемые виджеты, которые будут автоматически сохранять изменённые поля. Базовым классом для них будет App.views.EditableWidget.
- На 3-й - добавим в некоторые графики функцию zoom, что потребует добавления контролов, котрые будут появляться по наведению мыши, а также создадим виджет реального времени TweetsWidget.
С каждым наследованием функции render() и initialize() оборачивается в соответствующие функции дочернего класса. Дополнительное неудобство в том, что если нужно скомпоновать или обработать данные перед отрисовкой, нам придётся как-то передавать их в render() базового класса. Т.е. придётся добавлять аргументы, что выглядит некрасиво:
render: function(customData) {
if (customData === undefined) {
customData = {};
}
var data = customData;
data[model] = this.model.attributes;
this.$el.html(this.template(data));
}
Получившиеся связи методов показаны синим и зелёным:

Самое время вспомнить паттерн «шаблонный метод».
Оставим входные аргументы render() в покое. Не будем переопределять методы. Если нам нужно изменить данные - добавим хук для получения кастомных данных:
render: function() {
var data = {}
// Добавление кастомных данных от дочернего класса, если они есть:
if (this._preRenderHook !== undefined) {
_.extend(data, this._preRenderHook());
}
// Старая функциональность:
data[model] = this.model.attributes;
this.$el.html(this.template(data));
}
render: function() {
var data = {}
// Добавим данные дочернего класса, если они есть:
if (this._preRenderHook !== undefined) {
_.extend(data, this._preRenderHook());
}
// Старая функциональность:
data[model] = this.model.attributes;
this.$el.html(this.template(data));
// Элементы управления:
if (this._postRenderHook !== undefined) {
this._postRenderHook();
}
}
Получится иерархия без запутанных связей:
Конечно же, в случае сложной иерархии, мы можем захотеть переопределять хуки в дочерних классах, чтобы их расширить, или даже добавлять хуки в хуки. Но с этим лучше не слишком усердствовать, особенно если будущее приложения до конца не ясно. Преимущество в том, что сейчас мы можем сделать всё в виде плоской структуры.
В классической реализации паттерна «шаблонный метод» обычно предполагается, что базовый алгоритм уже определён. Однако, мы можем дополнять базовый алгоритм новыми хуками только для классов, которым эти хуки нужны. Для остальных - они будут пропускаться.
Полезности применения такого подхода:
- Мы можем добавлять хуки быстро, на любой стадии разработки. При этом они не повлияют на уже реализованные классы.
- Мы можем встраивать хуки в любое место в методе базового класса, а не только дополнять его в начале и в конце, как в случае с переопределением. Если хуков будет слишком много, это может запутать код метода - но зато мы можем легко контролировать очерёдность инициализации элементов виджета.
- Структура кода - плоская. Методы не вкладываются друг в друга при наследовании, а просто добавляются в базовый класс. То есть код гораздо легче понять.
Комментариев нет:
Отправить комментарий