Как стать автором
Обновить

Синхронизация представления с коллекцией

Время на прочтение5 мин
Количество просмотров1.7K
Во многих современных языках программирования и фреймворках есть специальные классы коллекций, которые умеют оповещать клиентов при каждом своем изменении. Во Flex этот класс носит имя ArrayCollection, в .Net — ObservableCollection, в ExtJS — Ext.util.MixedCollection и Ext.data.Store, в jWidgetJW.Collection. Такие структуры данных просто необходимы при разработке приложений по схеме MVC (Model, View, Controller). Наиболее часто они применяются в качестве модели для разного рода UI-компонентов: списков, таблиц, аккордионов и пр. В сложных приложениях коллекции нужны для связи нескольких слоев системы между собой.

Сегодня расскажу вам об одном оригинальном способе работы с коллекциями.


Типичное поведение связки модель-представление, где в качестве модели берется коллекция, заключается в их постоянной синхронизации. При этом схема получается, по меньшей мере, двухуровневая: коллекция и элемент.

Модель Представление
Коллекция Список, таблица
Элемент коллекции Строка списка, таблицы


Добавили элемент в коллекцию — нужно сразу же добавить строку в список. Удалили элемент из коллекции — нужно тут же удалить соответствующую строку из списка. И так далее.

Как работают коллекции? Обычно, программисту дают возможность подписываться на ряд трививальных событий:

  • added (вставили элемент)
  • removed (удалили элемент)
  • replaced (заменили элемент в указанной ячейке)
  • moved (перенесли элемент из одного места в другое)
  • cleared (удалили все элементы)


Более продвинутые реализации, во избежание потерь производительности, предусматривают также события:

  • reordered (отсортировали элементы)
  • filtered (отфильтровали элементы)
  • resetted (поменяли коллекцию произвольным образом)


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




Рассмотрим простой пример. Допустим, мы создаем AJAX-версию Живого Журнала, то есть такой блог, где публикация страницы, ее редактирование, комментирование и пр. действия осуществляются без перезагрузки страницы. В качестве примера коллекции возьмем список статей. Статьи четко упорядочены по дате публикации, то есть у каждой есть свой индекс — место в общем списке. Наиболее старая статья имеет индекс 0, наиболее свежая N — 1, где N — количество статей. Нужно учесть, что всякая статья имеет несколько представлений:
  1. Краткое содержание статьи в основной части блога
    image
  2. Список «На этой странице» (боковая панель)
    image
  3. Статья влияет на отображение списка меток
    image
  4. Статья влияет на отображение календаря
    image


Когда автор публикует статью, она добавляется в конец списка. После этого выбрасывается событие «added» с параметром (index = N), что обозначает, что элемент был добавлен перед элементом с индексом N, т.е. в конец коллекции. Поскольку событие выбрасывается после вставки элемента в коллекцию, клиенты могут получить содержимое статьи, запросив его у коллекции по индексу. Поймав это событие, основная часть блога показывает краткое содержание новой статьи, «На этой странице» добавляет себе новую строчку, метки изменяют свои размеры, а календарь превращает одну из дат в ссылку.

Когда автор удаляет статью, она извлекается из коллекции, а все элементы после нее сдвигаются на один индекс назад. После этого выбрасывается событие «removed» с параметром (index, article). То есть пока событие обрабатывается, статья еще жива в параметре article, чтобы клиенты могли эту статью проанализировать (посмотреть метки и дату, например), а после обработки статья удаляется и память очищается. Поймав это событие, основная часть блога и список «На этой странице» удаляют соответствующие строчки, метки меняют свои размеры, а календарь удаляет ссылку на дату (если в этот день не было опубликовано других статей).

Когда автор изменяет статью, она перебрасывается в конец коллекции, в результате чего выбрасывается событие «moved», и представления снова обновляются тем или иным путем.

Ну и так далее...




В общем, коллекция меняется и всех об этом оповещает, а вся ответственность за изменения на веб-странице блога лежит на классах представлений. Примерно так и диктует нам схема MVC.

Замечание: Иногда еще есть событие «updated» — изменили элемент (не заменили целиком, а просто поменяли какое-то свойство существующего элемента), — но я считаю, что это событие нужно слушать не у коллекции, а у самих элементов; автоматизация этого события влечет большую потерю производительности.

Далее мы сфокусируемся на первых пяти событиях, как наиболее часто используемых: added, removed, replaced, moved, cleared.

Я работал на крупных проектах, где вся архитектура строилась целиком и полностью на MVC, и у меня есть определенный опыт в этой сфере. Я узнал, что обработка этих пяти событий обычно выполняется совершенно одинаково для большинства представлений. Кроме того, алгоритмы обработки этих событий сильно пересекаются друг с другом, в результате чего возникает тонна повторяющегося кода. Классы представлений сразу же получались громоздкими и трудноподдерживаемыми.

Вскоре стало понятно, что для большинства представлений можно задать всего несколько простых сценариев, которых будет достаточно для обработки всех событий. Причем эти сценарии в рамках одного представления не имеют между собой ничего общего, и выполняют совершенно конкретные и независимые атомарные операции. Далее, обработка всех событий коллекции однозначно описывается на основе этих простых сценариев, и для всех представлений выглядит совершенно одинаково, следовательно, она может быть реализована всего один раз в некотором вспомогательном классе. Этот вспомогательный класс работает по паттерну «стратегия», то есть в качестве параметров принимает на вход алгоритмы — те самые атомарные и независимые сценарии, уникальные для каждого представления.

Так родился новый паттерн. Правильно было бы назвать его «Collection View Facilitator» (Помощник Представления Коллекции), но мне почему-то нравится просто «Syncher».

Суть заключается в следующем. Все, что требуется от разработчика для реализации очередного представления какой-либо коллекции — определить следующие сценарии:

Семантика Описание Пример
Creator(Data):View Создать элемент представления Создать строку таблицы
Inserter(View, Index) Вставить существующий элемент представления в указанное место Вставить строку в таблицу
Remover(Index):View Удалить элемент представления из указанного места Удалить строку из таблицы
Destroyer(View) Уничтожить элемент представления Уничтожить строку таблицы
Clearer():Array of View Удалить все элементы из коллекции и вернуть их Удалить все строки таблицы и вернуть их


Очевидно, что эти алгоритмы очень просты и совершенно независимы между собой (кроме, может быть, последнего — но он необходим для оптимизации). Реализовать их не составляет труда. Но их достаточно, чтобы синхронизироваться с некоторой коллекцией, притом без каких-либо потерь производительности. Смотрите, как проста и эффективна теперь обработка стандартных событий коллекции:

  • added(index, data)
    1. view = Creator(data)
    2. Inserter(view, index)

  • removed(index, data)
    1. view = Remover(index)
    2. Destroyer(view)

  • replaced(index, oldData, newData)
    1. oldView = Remover(index)
    2. Destroyer(oldView)
    3. newView = Creator(newData)
    4. Inserter(newView, index)

  • moved(fromIndex, toIndex)
    1. view = Remover(fromIndex)
    2. Inserter(view, toIndex)

  • cleared()
    1. views = Clearer()
    2. foreach (view in views) Destroyer(view)



А теперь объединяем все это во вспомогательный класс и радуемся! Вот реализация этой штуки для JW.Collection: JW.Syncher. А вот тест, который можно рассмотреть в качестве примера: JW.Tests.Util.SyncherTestCase.

Может быть, можно придумать решение и для событий reordered, filtered и resetted, но до меня оно пока не дошло (возможно, потому что очень редко приходилось использовать их по назначению).
Теги:
Хабы:
Всего голосов 4: ↑2 и ↓20
Комментарии2

Публикации

Истории

Ближайшие события

19 августа – 20 октября
RuCode.Финал. Чемпионат по алгоритмическому программированию и ИИ
МоскваНижний НовгородЕкатеринбургСтавропольНовосибрискКалининградПермьВладивостокЧитаКраснорскТомскИжевскПетрозаводскКазаньКурскТюменьВолгоградУфаМурманскБишкекСочиУльяновскСаратовИркутскДолгопрудныйОнлайн
24 – 25 октября
One Day Offer для AQA Engineer и Developers
Онлайн
25 октября
Конференция по росту продуктов EGC’24
МоскваОнлайн
26 октября
ProIT Network Fest
Санкт-Петербург
7 – 8 ноября
Конференция byteoilgas_conf 2024
МоскваОнлайн
7 – 8 ноября
Конференция «Матемаркетинг»
МоскваОнлайн
15 – 16 ноября
IT-конференция Merge Skolkovo
Москва
25 – 26 апреля
IT-конференция Merge Tatarstan 2025
Казань