Во многих современных языках программирования и фреймворках есть специальные классы коллекций, которые умеют оповещать клиентов при каждом своем изменении. Во Flex этот класс носит имя ArrayCollection, в .Net — ObservableCollection, в ExtJS — Ext.util.MixedCollection и Ext.data.Store, в jWidget — JW.Collection. Такие структуры данных просто необходимы при разработке приложений по схеме MVC (Model, View, Controller). Наиболее часто они применяются в качестве модели для разного рода UI-компонентов: списков, таблиц, аккордионов и пр. В сложных приложениях коллекции нужны для связи нескольких слоев системы между собой.
Сегодня расскажу вам об одном оригинальном способе работы с коллекциями.
Типичное поведение связки модель-представление, где в качестве модели берется коллекция, заключается в их постоянной синхронизации. При этом схема получается, по меньшей мере, двухуровневая: коллекция и элемент.
Добавили элемент в коллекцию — нужно сразу же добавить строку в список. Удалили элемент из коллекции — нужно тут же удалить соответствующую строку из списка. И так далее.
Как работают коллекции? Обычно, программисту дают возможность подписываться на ряд трививальных событий:
Более продвинутые реализации, во избежание потерь производительности, предусматривают также события:
Все рассуждения относительно связки модель-представление также верны и для связки модель-контроллер. Вторая связка необходима при реализации сложных систем, когда у одной модели есть несколько представлений.
Рассмотрим простой пример. Допустим, мы создаем AJAX-версию Живого Журнала, то есть такой блог, где публикация страницы, ее редактирование, комментирование и пр. действия осуществляются без перезагрузки страницы. В качестве примера коллекции возьмем список статей. Статьи четко упорядочены по дате публикации, то есть у каждой есть свой индекс — место в общем списке. Наиболее старая статья имеет индекс 0, наиболее свежая N — 1, где N — количество статей. Нужно учесть, что всякая статья имеет несколько представлений:
Когда автор публикует статью, она добавляется в конец списка. После этого выбрасывается событие «added» с параметром (index = N), что обозначает, что элемент был добавлен перед элементом с индексом N, т.е. в конец коллекции. Поскольку событие выбрасывается после вставки элемента в коллекцию, клиенты могут получить содержимое статьи, запросив его у коллекции по индексу. Поймав это событие, основная часть блога показывает краткое содержание новой статьи, «На этой странице» добавляет себе новую строчку, метки изменяют свои размеры, а календарь превращает одну из дат в ссылку.
Когда автор удаляет статью, она извлекается из коллекции, а все элементы после нее сдвигаются на один индекс назад. После этого выбрасывается событие «removed» с параметром (index, article). То есть пока событие обрабатывается, статья еще жива в параметре article, чтобы клиенты могли эту статью проанализировать (посмотреть метки и дату, например), а после обработки статья удаляется и память очищается. Поймав это событие, основная часть блога и список «На этой странице» удаляют соответствующие строчки, метки меняют свои размеры, а календарь удаляет ссылку на дату (если в этот день не было опубликовано других статей).
Когда автор изменяет статью, она перебрасывается в конец коллекции, в результате чего выбрасывается событие «moved», и представления снова обновляются тем или иным путем.
Ну и так далее...
В общем, коллекция меняется и всех об этом оповещает, а вся ответственность за изменения на веб-странице блога лежит на классах представлений. Примерно так и диктует нам схема MVC.
Замечание: Иногда еще есть событие «updated» — изменили элемент (не заменили целиком, а просто поменяли какое-то свойство существующего элемента), — но я считаю, что это событие нужно слушать не у коллекции, а у самих элементов; автоматизация этого события влечет большую потерю производительности.
Далее мы сфокусируемся на первых пяти событиях, как наиболее часто используемых: added, removed, replaced, moved, cleared.
Я работал на крупных проектах, где вся архитектура строилась целиком и полностью на MVC, и у меня есть определенный опыт в этой сфере. Я узнал, что обработка этих пяти событий обычно выполняется совершенно одинаково для большинства представлений. Кроме того, алгоритмы обработки этих событий сильно пересекаются друг с другом, в результате чего возникает тонна повторяющегося кода. Классы представлений сразу же получались громоздкими и трудноподдерживаемыми.
Вскоре стало понятно, что для большинства представлений можно задать всего несколько простых сценариев, которых будет достаточно для обработки всех событий. Причем эти сценарии в рамках одного представления не имеют между собой ничего общего, и выполняют совершенно конкретные и независимые атомарные операции. Далее, обработка всех событий коллекции однозначно описывается на основе этих простых сценариев, и для всех представлений выглядит совершенно одинаково, следовательно, она может быть реализована всего один раз в некотором вспомогательном классе. Этот вспомогательный класс работает по паттерну «стратегия», то есть в качестве параметров принимает на вход алгоритмы — те самые атомарные и независимые сценарии, уникальные для каждого представления.
Так родился новый паттерн. Правильно было бы назвать его «Collection View Facilitator» (Помощник Представления Коллекции), но мне почему-то нравится просто «Syncher».
Суть заключается в следующем. Все, что требуется от разработчика для реализации очередного представления какой-либо коллекции — определить следующие сценарии:
Очевидно, что эти алгоритмы очень просты и совершенно независимы между собой (кроме, может быть, последнего — но он необходим для оптимизации). Реализовать их не составляет труда. Но их достаточно, чтобы синхронизироваться с некоторой коллекцией, притом без каких-либо потерь производительности. Смотрите, как проста и эффективна теперь обработка стандартных событий коллекции:
А теперь объединяем все это во вспомогательный класс и радуемся! Вот реализация этой штуки для JW.Collection: JW.Syncher. А вот тест, который можно рассмотреть в качестве примера: JW.Tests.Util.SyncherTestCase.
Может быть, можно придумать решение и для событий reordered, filtered и resetted, но до меня оно пока не дошло (возможно, потому что очень редко приходилось использовать их по назначению).
Сегодня расскажу вам об одном оригинальном способе работы с коллекциями.
Типичное поведение связки модель-представление, где в качестве модели берется коллекция, заключается в их постоянной синхронизации. При этом схема получается, по меньшей мере, двухуровневая: коллекция и элемент.
Модель | Представление |
Коллекция | Список, таблица |
Элемент коллекции | Строка списка, таблицы |
Добавили элемент в коллекцию — нужно сразу же добавить строку в список. Удалили элемент из коллекции — нужно тут же удалить соответствующую строку из списка. И так далее.
Как работают коллекции? Обычно, программисту дают возможность подписываться на ряд трививальных событий:
- added (вставили элемент)
- removed (удалили элемент)
- replaced (заменили элемент в указанной ячейке)
- moved (перенесли элемент из одного места в другое)
- cleared (удалили все элементы)
Более продвинутые реализации, во избежание потерь производительности, предусматривают также события:
- reordered (отсортировали элементы)
- filtered (отфильтровали элементы)
- resetted (поменяли коллекцию произвольным образом)
Все рассуждения относительно связки модель-представление также верны и для связки модель-контроллер. Вторая связка необходима при реализации сложных систем, когда у одной модели есть несколько представлений.
Рассмотрим простой пример. Допустим, мы создаем AJAX-версию Живого Журнала, то есть такой блог, где публикация страницы, ее редактирование, комментирование и пр. действия осуществляются без перезагрузки страницы. В качестве примера коллекции возьмем список статей. Статьи четко упорядочены по дате публикации, то есть у каждой есть свой индекс — место в общем списке. Наиболее старая статья имеет индекс 0, наиболее свежая N — 1, где N — количество статей. Нужно учесть, что всякая статья имеет несколько представлений:
- Краткое содержание статьи в основной части блога
- Список «На этой странице» (боковая панель)
- Статья влияет на отображение списка меток
- Статья влияет на отображение календаря
Когда автор публикует статью, она добавляется в конец списка. После этого выбрасывается событие «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)
- view = Creator(data)
- Inserter(view, index)
- removed(index, data)
- view = Remover(index)
- Destroyer(view)
- replaced(index, oldData, newData)
- oldView = Remover(index)
- Destroyer(oldView)
- newView = Creator(newData)
- Inserter(newView, index)
- moved(fromIndex, toIndex)
- view = Remover(fromIndex)
- Inserter(view, toIndex)
- cleared()
- views = Clearer()
- foreach (view in views) Destroyer(view)
А теперь объединяем все это во вспомогательный класс и радуемся! Вот реализация этой штуки для JW.Collection: JW.Syncher. А вот тест, который можно рассмотреть в качестве примера: JW.Tests.Util.SyncherTestCase.
Может быть, можно придумать решение и для событий reordered, filtered и resetted, но до меня оно пока не дошло (возможно, потому что очень редко приходилось использовать их по назначению).