Всего лишь визуальное представление
Каждый смертный все же видит
Только то, что хочет видеть,
Отметая остальное.
Ля-ля-ля…
Ранее нас учили не писать программы одним большим куском, а использовать принцип «разделяй и властвуй» и разбивать программу на модули. У каждого молу-ля есть свои собственные обязанности; модуль (или класс) считается четко определенным, если у него имеется одна четко обозначенная обязанность.
Но как только вы разбиваете программу на различные модули, основанные на обязанностях, вы сталкиваетесь с новой проблемой. Каким образом объекты общаются друг с другом на стадии выполнения программы? Как вы управляете логическими зависимостями между ними? Другими словами, как вы осуществляете синхронизацию изменений состояния (или обновление значений данных) различных объектов? Этой работе должна быть присуща четкость и гибкость – мы не хотим, чтобы они узнали друг о друге слишком много. Мы хотим, чтобы каждый модуль был похож на героя песни Саймона и Гарфункеля и видел только то, что хочет увидеть.
Начнем с концепции события. Событие представляет собой специальное сообщение, в котором говорится: «Только что случилось нечто интересное» (разумеется, с точки зрения наблюдателя). Мы можем использовать события, чтобы сигнализировать одному объекту об изменениях, произошедших с другим объектом, в которых последний может быть заинтересован.
Подобное использование событий сводит к минимуму связывание между двумя объектами – отправителю события не нужно обладать явной информацией о получателе. На самом деле могут существовать и множественные получатели, каждый из которых сосредоточен на собственном перечне основных операций (отправитель же находится в блаженном неведении относительно этого факта).
Однако при использовании событий необходимо соблюдать некоторую осторожность. Например, в одной из ранних версий Java одна подпрограмма получила все события, предназначенные для специфического приложения. Это не совсем подходит для облегчения сопровождения или развития программы.
Протокол «Публикация и подписка»
Почему считается дурным тоном пропускать все события через одну-единственную программу? Потому что при этом нарушается инкапсулирование объекта – теперь этой подпрограмме приходится получать сокровенную информацию о взаимодействии между многими объектами. Это также способствует увеличению связывания, а мы пытаемся его уменьшить. Поскольку и самим объектам приходится получать информацию об этих событиях, то, по всей вероятности, вы собираетесь нарушить принцип DRY, принцип ортогональности и, может быть, некоторые разделы Женевской конвенции. Быть может, вам случалось видеть подобные программы – их доминантой является огромный оператор case или многообразная конструкция if-then. Мы можем сделать это изящнее.
Объекты должны иметь возможность регистрации только для приема событий, которые им нужны, и никогда не должны посылать события, которые им не нужны. Мы не хотим, чтобы наши объекты подверглись спаммингу! Вместо этого мы можем воспользоваться протоколом типа «публикация и подписка», который представлен на рисунке 5.4 с помощью диаграммы последовательностей на языке UML [34].
На блок-схеме последовательности показан поток сообщений между несколькими объектами, которые располагаются по столбцам. Каждое сообщение обозначено стрелкой с текстом, идущей от столбца отправителя к столбцу получателя. Звездочка у текста означает, что возможна посылка более одного сообщения данного типа.
Рис. 5.4. Протокол «Публикация и подписка»
Если нам интересны определенные события, которые генерируются объектом Publisher (Издатель), то все, что нам нужно, – это зарегистрироваться. Объект Publisher отслеживает все заинтересованные объекты Subscriber (Подписчик); когда объект Publisher генерирует событие, представляющее интерес, он, в свою очередь обращается к каждому объекту Subscriber, извещая их о том, что данное событие произошло.
На эту тему существует несколько вариаций, отражающих другие стили обмена данными. Объекты могут использовать протокол «Публикация и подписка» на одноранговой основе (как показано выше), а также «программную шину», где централизованный объект поддерживает базу данных «слушателей» и осуществляет соответствующую диспетчеризацию. Вы даже можете получить схему, в которой критические события транслируются ко всем «слушателям» – как зарегистрированным, так и незарегистрированным. Одна из возможных реализаций событий в распределенной среде иллюстрируется службой сообщений CORBA, описанной во врезке «Служба событий CORBA» (см. ниже).
Можно использовать протокол «Публикация и подписка» для реализации очень важного принципа проектирования: отделения самой модели от ее визуальных представлений. Начнем с примера графического интерфейса, используя конструкцию на языке Smalltalk, где зародилась данная концепция.
Принцип «модель-визуальное представление-контроллер»
Предположим, что есть приложение – электронная таблица. В дополнение к числам, расположенным в самой таблице, также имеется график, отображающий числа на гистограмме и диалоговое окно суммы с накоплением, отображающим сумму чисел в некотором столбце таблицы.
Служба событий CORBA позволяет объектам-участникам отправлять и получать уведомления о событиях через общую шину, так называемый канал событий. Канал событий принимает решение по обработке событий, а также осуществляет разделение производителей и потребителей событий. Он работает в двух основных режимах: «проталкивание» и «вытягивание».
В режиме «проталкивания» поставщики событий информируют канал событий о том, что событие произошло. Затем канал автоматически распространяет это событие ко всем объектам-клиентам, которые зарегистрировались, выражая свой интерес.
В режиме «вытягивания» клиенты периодически опрашивают канал событий, который в свою очередь, опрашивает поставщика, предлагающего данные о событии в соответствии с запросом.
Хотя служба событий CORBA может использоваться для реализации всех событийных моделей, описанных в данном разделе, ее можно рассматривать и в другом качестве. CORBA облегчает связь между объектами, написанными на различных языках программирования и выполняющимися на географически рассредоточенных машинах с различными архитектурами. Находясь на верхнем уровне CORBA, служба событий предоставляет вам способ, отличающийся отсутствием связанности и позволяющий взаимодействовать с приложениями, разбросанными по всему миру и написанными людьми, которых вы никогда не встречали, и пишущими на языках, о которых вы и знать не знаете.
Очевидно, мы не хотим иметь три отдельных копии одних и тех же данных. Поэтому мы создаем модель – сами данные и обычные операции для их обработки. Затем мы можем создать отдельные визуальные представления, которые отображают данные различными способами: в виде электронной таблицы, графика или поля суммы с накоплением. Каждое из этих визуальных представлений может располагать собственными контроллерами. Например, график может располагать неким контроллером, позволяющим приближать и отдалять объекты, осуществлять панорамирование относительно данных. Ни одно из этих средств не оказывает влияния на данные, только на это представление.
Это и является ключевым принципом, на котором основана парадигма «модель-визуальное представление-контроллер»: отделение модели от графического интерфейса, ее представляющего, и средств управления визуальным представлением [35].
Действуя подобным образом, вы можете извлечь пользу из некоторых интересных возможностей. Вы можете поддерживать множественные визуальные представления для одной и той же модели данных. Вы можете использовать обычные средства просмотра со многими различными моделями данных. Вы даже можете поддерживать множественные контроллеры для обеспечения нетрадиционных механизмов ввода данных.
Подсказка 42: Отделяйте визуальные представления от моделей
Ослабляя связанность между моделью и ее визуальным представлением/контроллером, вы приобретаете большую гибкость практически за бесценок. На самом деле, эта методика является одним из важнейших способов сохранения обратимости (см. «Обратимость»).
Хорошим примером принципа «модель-визуальное представление-контроллер» является графический элемент в древовидной схеме Java. Элемент, отображающий дерево, активизируемое щелчком мыши, в действительности представляет собой набор нескольких различных классов, организованных по шаблону «модель-визуальное представление-контроллер».
Все, что вам нужно сделать для получения полнофункционального элемента дерева, – это обеспечить источник данных, который соответствует интерфейсу TreeModel. Ваша программа становится моделью дерева.
Визуальное представление создается классами TreeCellRenderer и TreeCellEditor, которые могут быть унаследованы и настроены для обеспечения различных цветов, шрифтов и пиктограмм в графическом элементе. JTree действует в качестве контроллера для элемента дерева и обеспечивает некоторую общую функциональную возможность просмотра.
Осуществив разделение модели и ее визуального представления, мы серьезно упростили процесс программирования. Уже не нужно беспокоиться об элементе дерева. Вместо этого необходимо предоставить источник данных.
Предположим, к вам подходит вице-президент фирмы и высказывает пожелание, чтобы вы быстро написали приложение, которое позволяет ему управлять структурной схемой фирмы, содержащейся в унаследованной базе данных на мэйнфрейме. Просто напишите оболочку, которая получает данные с мэйнфрейма, представляет ее в виде TreeModel, и – «Вуаля!» – у вас имеется полнофункциональный элемент дерева.
Теперь можете капризничать и начать использовать классы средств просмотра; вы можете изменять представление узлов и использовать специальные пиктограммы, шрифты или цвета. Когда вице-президент вернется к вам и скажет, что новые корпоративные стандарты требуют использования для некоторых служащих пиктограммы «Веселый Роджер», то вы можете внести изменения в TreeCellRenderer, не затрагивая другие программы.
Отходя от графических интерфейсов
Хотя принцип «модель-визуальное представление-контроллер» обычно реализуется в контексте графического интерфейса, на самом деле он является универсальной методикой программирования. Визуальное представление – это некая интерпретация модели (возможно, подмножества), и она не обязана быть графической. Контроллер в большей части является механизмом координации и не должен ассоциироваться с устройством ввода любого типа.
• Модель. Абстрактная модель данных, представляющая целевой объект. Модель не располагает непосредственной информацией о любых визуальных представлениях или контроллерах.
• Визуальное представление. Способ интерпретации модели. Оно подписывается на изменения в модели и логические события, приходящие от контроллера.
• Контроллер. Способ контроля визуального представления и снабжения модели новыми данными. Он осуществляет публикацию событий для модели и визуального представления.
Рассмотрим пример с текстовым интерфейсом.
Игра в бейсбол представляет собой уникальное явление. Где еще можно найти такие пустяки, как «самый результативный матч, сыгранный во вторник под дождем при искусственном освещении между командами, названия которых начинаются с гласной буквы»? Предположим, что нам поручили разработать программу для помощи бесстрашным дикторам, которым по должности полагается сообщать счет, статистику и прочие мелочи.
Ясно, что нам необходима информация о матче, который проходит в настоящее время – играющие команды, условия, игрок, принимающий подачу, счет и т. д. Эти факты образуют наши модели; они будут обновляться по мере поступления новой информации (смена подающего, выбывание игрока, начался дождь…).
Затем у нас появится ряд объектов – визуальных представлений, которые будут использовать эти модели. Один объект должен наблюдать за набираемыми очками – для обновления текущего счета. Другой объект может получать уведомления о новых игроках, отбивающих мяч, и извлекать краткую справку об их статистических показателях за год. Третий объект может просматривать данные и проверять, не установлен ли мировой рекорд. Можно даже использовать средство просмотра «мелочей», которое несет ответственность за придумывание сверхъестественных и бесполезных фактов, щекочущих нервы зрителей.
Рис. 5.5. Комментирование бейсбольного матча. Средства просмотра являются подписчиками модели.
Но мы не хотим, чтобы несчастный диктор работал со всеми этими окнами непосредственно. Вместо этого мы сделаем так, чтобы каждое из окон генерировало извещения об «интересных» событиях, и обеспечим возможность планирования показа с помощью некоторого высокоуровневого объекта [36].
Эти объекты (средства просмотра) внезапно стали моделями высокоуровневого объекта, который сам по себе может стать моделью для различных форматирующих средств просмотра. Одно такое средство просмотра могло бы создать сценарий для телесуфлера, с которым работает диктор, второе могло бы генерировать заставки непосредственно на спутниковом канале, а третье могло бы осуществлять обновление web-страниц телевизионной сети или бейсбольной команды (см. рис. 5.5).
Подобная сеть «модель-средство просмотра» является универсальной (и весьма ценной) методикой проектирования. Каждый канал связи осуществляет отделение исходных данных от событий, их породивших; каждое новое средство просмотра есть некая абстракция. И поскольку отношения представляют собой сеть (а не линейную цепь), то мы обладаем большой гибкостью. Каждая модель может включать в себя много средств просмотра, а одно средство просмотра может работать со многими моделями.
В усовершенствованных системах, наподобие описанной выше, полезно иметь окна отладки – специализированные окна, которые отображают подробности модели. Дополнение системы средством трассировки отдельных событий также способствует существенной экономии времени.
Все такой же связанный (после стольких лет)
Несмотря на то, что мы добились уменьшения связанности, прослушивающие процессы и генераторы событий (подписчики и издатели) все равно обладают некоторой информацией друг о друге. Например, в языке Java они обязаны прийти к соглашению об общих определениях интерфейса и вызовах.
В следующем разделе мы рассмотрим способы дальнейшего уменьшения степени связанности при помощи формы «публикация и подписка», в которой ни один из участников не должен знать друг о друге или обращаться напрямую друг к другу.
• Ортогональность
• Обратимость
• Несвязанность и закон Деметера
• Доски объявлений
• Все эти сочинения
29. Предположим, что имеется система бронирования авиабилетов, основанная на следующем принципе формирования авиарейса:
public interface Flight {
//Return false if flight full.
public Boolean addPassenger(Passenger p);
public void addToWaitlJst(Passenger p);
public int getFlightCapacity();
public int getNumPassengers();
}
Если вы добавляете имя пассажира в лист ожидания авиарейса, то при появлении вакантного места ему будет предложено воспользоваться этим рейсом автоматически.
Чтобы составить расписание дополнительных рейсов, требуется большая работа с отчетами, заключающаяся в выискивании рейсов, количество мест на которых меньше или равно числу проданных билетов. Это срабатывает, но занимает много времени.
Нам хотелось бы обладать большей гибкостью при обработке данных о пассажирах в листе ожидания и как-то решить проблемы с этим огромным отчетом – его формирование занимает слишком много времени. Воспользуйтесь идеями, изложенными в данном разделе, чтобы спроектировать этот интерфейс по-новому.