Объектно-ориентированное программирование и программная инженерия

События

Разбить на страницы
Показывать лекцию целиком

Кто в ответе?

В используемом до сих пор стиле программирования порядок выполнения операций определяла программа. Он следовал из ее собственного сценария, определяемого управляющими структурами: последовательностью, условным оператором и оператором цикла. Внешний мир мог сказать свое слово — через интерфейс пользователя, доступ к базе данных, другой ввод, воздействующий на условия, циклы, динамическое связывание. Но последнее слово оставалось за программой, которая решала, где вычислять эти условия.

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

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

Такой архитектурный стиль — в конечном счете, еще одна управляющая структура, которую следует добавить к предыдущему каталогу структур, — известна также под именем "издатели-подписчики", метафора, отражающая возможное разделение ролей между элементами ПО.

  • Некоторые элементы, издатели, могут включать события во время выполнения.
  • Некоторые элементы, подписчики, подписываются на определенные типы событий, указывая, какие службы должны вызываться в ответ на возникновение того или иного события.
  • Эти роли не являются исключительными, поскольку некоторые подписчики могут включать собственные события. Заметьте, "событие" является программной концепцией: даже когда события возбуждаются вне ПО — нажатие кнопки мыши, измерение датчика, прибытие сообщения, — для того чтобы быть обработанными, они должны транслироваться в программные события. ПО может включать свои собственные события, не связанные с внешними воздействиями.

    Программирование, управляемое событиями, применимо во многих различных областях. Оно с успехом применяется в графическом интерфейсе пользователя — GUI, что и станет нашим первым примером.

    Управляемое событиями GUI-программирование

    Старый добрый ввод:

    (рис 7.1)

    До появления GUI программы вводили данные, приходящие от некоторого последовательного посредника, например, программа могла читать последовательность строк, обрабатывая каждую из них соответствующим образом:

    from
      read_line
      count : = 0
    until
      exhausted
    loop
      count := count + 1
        — Сохранить last_line в позиции count в Result:
    Result [count] := last_line
      read_line
    end
    

    Здесь read_line пытается читать следующую строку ввода, сохраняя ее в last_line, а значение exhausted принимает значение true, если при выполнении read_line нет больше строк для ввода.

    При такой схеме управление остается за программой : она решает, когда ей нужен очередной ввод данных. Остальной мир — здесь файл или пользователь, печатающий строки на терминале, — должен обеспечить затребованный программой ввод.

    Современный интерфейс

    Добро пожаловать в современный мир. Если программа использует GUI, то пользователю на каждом шаге предоставляется выбор, что же он хочет делать, включая выбор, не связанный с программой, поскольку он может переключиться на работу в другом окне с другой программой, например, послать письмо по электронной почте.

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

    Ожидается, что пользователь выполняет некоторые действия по вводу данных, а наша программа соответствующим образом реагирует на них. Действиями могут быть печать текста в текстовом окне, щелчок по кнопке, выбор пункта меню.

    Но какое действие будет первым, и случится ли вообще хоть что-нибудь?

    (рис 7.2) Интерфейс программы

    Мы не знаем.

    Конечно, мы могли бы использовать большой оператор if... then ... elseif... end или конструкцию выбора со многими ветвями, перечисляя все возможности:

    inspect
      user_action
    when "Нажата кнопка Stop" then
      "Завершить выполнение"
    when "Введен текст в поле Class Name" then
    "В соответствующее окно (слева вверху) вывести текст класса, имя которого
    задано в текстовом поле"
    when    …Другие ветви…
    end
    

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

    Проектирование, управляемое событиями ("издатели-подписчики"), предлагает в такой ситуации совсем другую схему.

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

    (рис 7.3) Включение и обработка событий

    Стиль "Издатели-подписчики" полезен в различных областях приложения: GUI-программирование — это просто один из примеров. Другие примеры:

  • сети передачи данных, где возможна широковещательная передача, — один передатчик и много приемников;
  • управление процессом. Сюда входят программные системы, управляющие производственными процессами. В таких системах данные поступают от многочисленных датчиков, следящих за температурой, давлением и другими характеристиками процесса. Возникающие в результате события обрабатываются подготовленными элементами ПО.
  • Терминология

    Для рассматриваемого стиля программирования важно тщательно определить концепции, обращая, как обычно, внимание на различение типов и их экземпляров. События, издатели и подписчики

    Определение: событие

    Событие - это операция периода выполнения, производимая элементом ПО. В результате события некоторая информация становится доступной для потенциального использования другими элементами ПО, не указанными в операции.

    Это определение высвечивает отличительные свойства событий.

  • Событие сопровождается некоторой информацией: при щелчке мыши должна указываться позиция курсора, при изменении температуры — старая и новая температуры.
  • Всегда включается информация о том, какое событие произошло:
  • 5-го августа 1492 года Христфор Колумб отправился в плавание;
  • 5 минут назад (менее известное событие) я щелкнул левую кнопку моей мыши.
  • Обычно информации больше — куда отправился Колумб, каковы координаты мыши. Но иногда достаточно знать, что событие произошло, например, что истек срок ожидания.
  • Определенные элементы ПО могут использовать эту информацию.
  • Во всех случаях важно то, что само событие не знает своих получателей (писатель не знает читателей). В противном случае этот механизм не отличался бы от механизма вызова методов, подобно вызову x.f (a, b, c), который удовлетворяет всем другим свойствам определения — поставляет информацию (a, b, c), доступную элементу ПО (методу f ). Но когда вызывается метод, то явно указывается адресат вызова. При вызове событий все не так — информация посылается в межадресное пространство, где любой элемент может использовать ее в интересах своей работы.
  • Помните, что для наших целей событие — это операция ПО. Могут существовать внешние события — действия пользователя и прочее, — приводящие к возникновению программных событий, но и сама программа может создавать события в процессе работы.

    Рассмотрим связанную с этими понятиями терминологию, неформально уже встречавшуюся.

    Определения: включение, публикация, издатель, подписчик

    Включением (или публикацией) события является его выполнение. Элемент ПО, включающий событие, - издатель. Элемент ПО, использующий информацию события, - подписчик.

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

    В литературе встречаются синонимы приведенных выше терминов: подписчики называются "наблюдателями", отсюда образец "Наблюдатель", изучаемый в этой лекции. Говорят также о "слушателях" события. Для "издателей" также используется синоним, вытекающий из подобной риторики — "предмет наблюдения" .

    Аргументы и типы событий

    Нам необходимо имя для информации, приходящей — в соответствии с определением — с любым событием.

    Определение: аргумент

    Информация, связанная с событием, состоит из аргументов события.

    Термин "аргумент" указывает на сходство с аргументами метода. Продолжая это сходство, будем предполагать, что аргументы сгруппированы в упорядоченный список, подобно аргументам в вызове x.f (a, b, c) . Как и для методов, список может быть пустым, например, в случае события, указывающего на истечение срока ожидания.

    Как подписчики обнаруживают, что произошло интересующее их событие? Одна модель — опрос (повторяющаяся проверка). Это напоминает ситуацию с подписчиком журнала, который в день издания ходит к почтовому ящику, чтобы проверить, не пришел ли журнал. Вторая модель — уведомление: при включении события об этом уведомляются все подписчики.

    Модели для информации, распределенной в сети Интернет, классифицируются как "притяжение" (ожидание пользователей, заинтересованных в информации) и "проталкивание" (рассылка информации пользователям).

    Модель уведомления — более гибкая, и далее будем предполагать именно ее. Она может работать при условии, что подписчики заранее явно проявили свой интерес. Дело обстоит так же как с подписчиками журнала: на журнал надо заранее подписаться. Но на что можно подписаться? На событие подписаться нельзя, поскольку, по определению, это операция, выполняемая в ходе работы программы: до его появления оно не существует, а после — уже поздно.

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

    Все события одного типа имеют один и тот же список аргументов. Например, список аргументов для события — щелчок левой кнопки мыши — включает координаты мыши в момент щелчка — два целых числа. Во многом концепция заимствована у методов, обладающих сигнатурой — списком типов аргументов. Процедура print (v: VALUE; f: FORMAT) имеет сигнатуру [VALUE, FORMAT] — список типов. Это понятие расширяется на типы события.

    Определения: тип события, сигнатура

    Любое событие принадлежит определенному типу события.

    Все события данного типа события имеют одну и ту же сигнатуру. Например:

  • сигнатура события "изменение температуры" может быть [REAL, REAL], чтобы представлять старое и новое значения;
  • событие "левый щелчок мыши" может иметь сигнатуру [INTEGER, INTEGER] .
  • Можно также взять событие "одиночный щелчок мыши" с третьим компонентом сигнатуры, указывающим, какая кнопка была нажата. В библиотеке EiffelVision применяется вариант, в котором добавлен еще один аргумент, показывающий, остается ли нажатой кнопка, - он полезен (особенно в играх) для джойстика и экзотичных устройств указателей;

  • хотя можно было бы определить тип события для каждой клавиши на клавиатуре, более удобно использовать один тип события "клавиша нажата" с сигнатурой [CHARACTER], где аргумент задает код клавиши;
  • для событий без аргументов, например, "исчерпано время ожидания", сигнатура пуста, как у методов без аргументов.
  • Всякий раз, когда издатель включает событие, он должен обеспечить значение каждого аргумента (если они есть): координаты мыши, код клавиши, температуры. И здесь наблюдается полная аналогия с вызовом метода, где при каждом вызове задаются фактические аргументы.

    Термин "тип события" может предполагать еще одну аналогию, где каждый тип соответствует типам ОО-программирования (классам с возможно родовыми параметрами), а каждое событие соответствует экземпляру класса (объектам). Но сравнение типов событий с программами - более показательное, тогда появление события данного типа соответствует вызову программы.

    При таком подходе событие — это не объект, а тип события — это не класс. Вместо этого можно ввести общее понятие для всех типов событий: класс, называемый ниже EVENT_TYPE, а конкретный тип события, например, "щелчок левой кнопки мыши" (абстракция левых щелчков — вроде того, что я в прошлый понедельник, будучи в расстроенных чувствах, щелкнул в ответ на запрос "Delete all?" ) рассматривать как объект. Как всегда, когда вы раздумываете, не ввести ли класс, критерием является "Можно ли эту абстракцию данных наполнить смыслом, определив множество хорошо понимаемых операций, применимых ко всем объектам класса?". В данном случае:

  • если бы мы решили создавать класс для типа события, его экземплярами были бы события одного типа, но у них не было бы полезных компонентов. Более точно, события имели бы собственные данные — аргументы, но они нуждались бы только в запросах для получения доступа к этим аргументам; команд здесь не было бы;
  • в противоположность такому подходу: если рассматривать конкретный тип события как объект, то появляется несколько хорошо определенных команд и запросов — включить событие с заданным набором аргументов, подписать данного подписчика на этот тип события, удалить подписчика из списка подписчиков, перечислить всех подписчиков, подсчитать число включений события данного типа и так далее. Это богатое множество компонентов характеризует полезный законный класс.
  • Не рассматривать каждое событие как объект полезно и с позиций производительности. Обычно во время выполнения создается большое число событий - каждое малое перемещение курсора включает событие, так что следует избегать создания всех соответствующих объектов. Это не освобождает нас от нагрузки, поскольку аргументы каждого события, представленные кортежем, должны быть записаны. В хорошей библиотеке GUI производительность улучшается за счет того, что для последовательности близких событий можно использовать один кортеж вместо десятков сотен.

    Полезно иметь термины для действий подписчиков с типами событий и событиями.

    Определения: подписать, зарегистрировать, обработать, захватить

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

    Когда элемент получает уведомление о событии, на которое он подписан, он обрабатывает (или захватывает ) событие, выполняя зарегистрированное действие.

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

    При регистрации подписчик задает некоторое действие, выполняемое в ответ на возникновение события данного типа. У действия должен быть способ получения аргументов события. Очевидный путь достижения цели — это указание при регистрации метода, чья сигнатура соответствует сигнатуре типа события. Тогда возникновение события данного типа будет причиной вызова метода, обрабатывающего событие, где аргументы события играют роль фактических аргументов вызова.

    Теперь у нас есть полная картина того, как работает программа, управляемая событиями.

    Схема управления событиями

    E1 Некоторые элементы, издатели, позволяют остальной системе узнать, какие типы событий они могут включать.

    E2 Некоторые элементы, подписчики, заинтересованы в обработке событий определенных типов. Они регистрируют соответствующие действия.

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

    В примере GUI:

    E1 Издателем является некоторый элемент ПО, который следит за устройствами ввода и включает события при определенных обстоятельствах, например, при нажатии клавиш клавиатуры или кнопок мыши. Обычно нет необходимости писать такое ПО, поскольку оно является частью библиотеки GUI - EiffelVision для Eiffel, Windows Forms для .NET, Swing для Java.

    E2 Подписчиком является любой элемент, которому требуется обработать GUI-события. Он регистрирует методы, выполняемые в ответ на события. Например, метод, сохраняющий файл, можно зарегистрировать для события щелчок мыши на кнопке с надписью "OK" в диалоге по сохранению файла.

    E3 Если во время выполнения пользователь щелкнет по кнопке OK, то это станет причиной выполнения метода — или методов, — зарегистрированных для данного типа события.

    Важное свойство этой схемы, проиллюстрированное на последнем рисунке: выделение двух сторон, участвующих в создании и обработке события, — подписчики и издатели ничего не знают друг о друге. Более точно, определение "события" требует, чтобы подписчики не знали других подписчиков. Остальное — дело методологии, и мы увидим, как различные архитектурные решения достигают результата по отношению к этому критерию.

    Ясное понимание различий

    Возможно, вы полагаете, что различие между типом события и самим событием очевидно, но в литературе эти понятия зачастую путаются, из-за чего простые вещи кажутся сложными. Это предупреждение должно помочь вам при изучении различных механизмов программирования, управляемого событиями.

    Следующий текст представляет часть документации .NET от Microsoft, представляющей обработку событий, чьи концепции отражены в языках C# и Visual Basic .NET

    Обзор событий

    События имеют следующие свойства.
  • Издатель определяет, когда событие включается; подписчики определяют, какие действия будут выполняться в ответ на событие.
  • Событие может иметь много подписчиков. Подписчик может обрабатывать события, приходящие от многих издателей.
  • События, не имеющие подписчиков, никогда не вызываются.
  • События широко используются для сигнализации о действиях пользователя, таких как щелчки по кнопкам или выбор в меню в графическом интерфейсе пользователя.
  • При наличии множества подписчиков вызов обработчиков события синхронизируется. Для асинхронного вызова смотри следующий раздел.
  • События могут использоваться для синхронизации потоков.
  • События в библиотеке классов .NET Framework основаны на делегате EventHandler и базовом классе EventArgs .
  • Здесь один и тот же термин "событие" в разных контекстах имеет разный смысл: иногда речь идет действительно о событиях, иногда о типах событий, иногда об обоих понятиях одновременно. Думаю, что вы согласитесь с моей интерпретацией. В частности:

  • невозможно подписываться на события (пункты 1, 5). Как мы видели, событие не существует, пока оно не будет возбуждено. Подписчик подписывается на тип события, уведомляя, что он хочет получать уведомление о каждом событии этого типа, когда оно произойдет во время выполнения;
  • в пункте 7 говорится о свойствах классов, описывающих типы событий. Заметьте, что в .NET каждый тип события объявляется как класс. Такой класс должен наследовать от класса EventHandler, представляющего классы, которые называются делегатами (delegate) — последние обеспечивают механизм, подобный агентам. Еще один класс — EventArgs — является родительским классом для классов, задающих аргументы события;
  • пункт 3 звучит загадочно, пока не осознаешь, что имеется в виду следующее: "Если тип события не имеет подписчиков, включение события этого типа не дает никакого эффекта". Все это описывает внутреннюю оптимизацию: обнаружив, что тип события не имеет подписчиков, механизм события удаляет избыточно возбужденные события, что в .NET влечет к созданию объекта для каждого события.
  • Возможность непонимания особенно ярко проявляется в двух местах.

  • В пункте 2 говорится: "Подписчик может обрабатывать множественные события от множественных издателей". Из этого комментария можно сделать вывод, что речь идет о сложной параллельной схеме вычислений, где подписчик захватывает события из различных мест одновременно, но реально это просто означает, что подписчик может регистрировать несколько типов событий и каждый издатель может включать события разных типов.
  • В пункте 5 устанавливается, что для события, имеющего множество подписчиков, вызов обработчиков события синхронизируется. Предложение просто пытается сказать, что когда множество подписчиков зарегистрировали один и тот же тип события, они обрабатывают соответствующие события синхронно.
  • Так что при изучении схем управления событий проверяйте, о чем идет речь — о событиях или о типах событий, и убедитесь (это одно из наших очередных наставлений), что в вашей документации по событиям используется правильная терминология.

    Контексты

    Подписчик при регистрации говорит: "Для события этого типа выполняй это действие". На практике может быть полезно, особенно для приложений GUI, уточнить высказывание: "Для события этого типа, встречающегося в данном контексте, выполняй это действие". Например:

  • "если пользователь нажал левую кнопку мыши на кнопке OK, сохрани файл";
  • "если мышь находится в этом окне, измени цвет границ на красный";
  • "если датчик показывает температуру выше 25° С, включи сигнал тревоги".
  • В первом случае "контекстом" является элемент управления, а событием — "щелчок мыши". Во втором контекст — окно, событие — появление мыши; в третьем контекст — датчик, событие — превышение режима.

    Для GUI-программирования контекстом обычно является элемент управления. Как показывает последний пример, понятие контекста более общее; контекст может быть любым условием с булевским значением. Примеры GUI являются специальным случаем, где булевское условие задает свойство, такое как "курсор установлен на этой кнопке" или "курсор находится в этом окне". Вот общее определение:

    Определение: контекст

    При управлении событиями контекст представляет булевское выражение, задаваемое подписчиком в момент регистрации, но вычисляемое в момент включения, так что регистрируемое действие будет выполняться только, когда контекст принимает значение True.

    Понятие контекста нам знакомо по обычному стилю программирования, не связанному с событиями: вспомните итератор, такой как do_if, который выполняет действия над всеми элементами структуры, удовлетворяющими некоторому условию.

    Это аналогично тому, как контекст позволяет подписчику установить, что его интересуют события данного типа, но необходимо, чтобы в момент включения выполнялось определенное условие.

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

    if "Курсор на значке Exit" then
      "Выполнить предусмотренные действия"
    end
    

    Удобнее отделять условие, задавая его вместе с типом события и действием.

    Требования "Публиковать-подписаться"

    Установив концепции, будем теперь заниматься поиском общего решения проблемы, проектируя архитектуру управления событиями. Начнем с ограничений, которым должно удовлетворять хорошее решение.

    Издатели и подписчики

    При проектировании архитектуры, поддерживающей парадигму "Публиковать-подписаться", следует рассмотреть следующие ограничения.

  • Издатели не должны знать, кто является подписчиком. Они включают события, но в соответствии с определением события не знают, кто может их обрабатывать. Типичным случаем издателя является библиотека GUI. Методы библиотеки знают, как обнаружить событие, инициированное пользователем, такое как "щелчок кнопки", но они ничего не должны знать о конкретных приложениях, реагирующих на эти события, и о том, как они реагируют. В приложении нажатие кнопки может сигнализировать о начале компиляции, запуске процесса оплаты, выключении завода или запуске ракеты. Но все это для библиотеки GUI — просто щелчок кнопки.
  • Любое событие, включаемое одним издателем, может поставляться нескольким подписчикам. Изменение температуры в системе управления завода может отражаться в местах, "наблюдающих" за этим типом события. Данные могут отражаться на обычном и на графическом дисплее, в службе безопасности, включающей определенные действия при нарушениях режима, в записях изменений в базе данных.
  • Подписчикам нет необходимости знать издателей. Это более строгое, но часто желательное требование. Подписчики знают о типах событий, на которые они подписываются, но не должны знать, откуда приходят события. Помните, что одна из целей проектирования, управляемого событиями, состоит в обеспечении гибкой архитектуры, позволяющей включать разных издателей и разных подписчиков, возможно, написанных разными людьми и в разные времена.
  • Желательно иметь возможность во время выполнения как проводить регистрацию, так и отменять ее. В обычной схеме регистрация выполняется в фазе инициализации приложения, где устанавливаются его параметры до начала "настоящего" выполнения. Но это не является обязательным требованием — гибкость может быть полезной.
  • Должно быть возможным задавать события, зависящие или не зависящие от контекста. Мы видели полезность связывания события с контекстом, но решение должно обеспечивать возможность не придумывать искусственный контекст и просто подписаться на событие независимо от того, где оно случилось.
  • Связывание издателей и подписчиков должно выполняться с минимальными усилиями. Схему с событиями часто требуется добавить в уже существующее приложение. Для связывания сторон требуется добавить некоторый код, часто называемый "склеивающим кодом": чем меньше клея — тем лучше.
  • Последнее требование является критическим для качества системной архитектуры, особенно когда целью является построение пользовательских интерфейсов: не должно быть так, чтобы проектирование ядра зависело от особенностей интерфейса. Это наблюдение непосредственно приводит к нашим следующим понятиям — модели и облику.

    Модель и облик

    При проектировании интерфейса мы не только не должны различать подписчиков и издателей, но и различать два дополняющих аспекта приложения.

    Определения: модель и облик программной системы

    Модель (называемая также бизнес-моделью ) является той частью программной системы, которая обрабатывает данные, представляющие информацию прикладной области.

    Облик - это представление части этой информации при взаимодействии системы с внешним окружением: человеком - пользователем системы, материальными устройствами, другим ПО.

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

    Модель является частью ПО — частью, имеющей дело с прикладной областью. Для платежной системы это та часть, которая обрабатывает информацию о служащих, их часах работы, начисляет зарплату, обновляет базу данных. Для системы управления полетом — прокладывает маршрут, вычисляет времена, авторизацию и прочее. Можно сказать, что модель — это часть, выполняющая "настоящую" работу, независимо от взаимодействия с пользователями ПО и остальным миром.

    Понятие "бизнес-модель" является более точным, но мы обычно предпочитаем говорить просто "модель". Одна из причин в том, что термин "бизнес" порождает неверные ассоциации (управление компанией, финансами), исключая такие области, как обработка текстов или управление полетами.

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

    Обычно программа предназначена для одной — возможно, весьма широкой — прикладной области, но обликов у программы может быть несколько. Хорошей практикой является рассмотрение программы с разных точек зрения. При наивном проектировании небольших программ не уделяется должного внимания этой проблеме. Но для серьезных систем необходимо планировать несколько обликов, таких как:

  • GUI-облик (обычно несколько);
  • Web-облик ("WUI"), позволяющий использовать систему через Web-браузер;
  • текстовый интерфейс — для ситуаций, когда графическая поддержка не нужна или невозможна;
  • интерфейс, основанный на batch-файлах, где подготовлен сценарий, заготовлены исходные данные и вывод пишется в заданное место. Это особенно полезно для интерактивных систем. Интерактивное тестирование трудно, оно требует присутствия людей, проводящих долгие сессии, пытаясь проверить различные комбинации. Вместо этого можно подготовить коллекцию сценариев (обычно записываемых во время сессии с участием человека), а затем выполнять их без участия человека;
  • облики, обеспечиваемые другими программами, которые выполняются локально; их функциональность доступна через API;
  • облики Web-сервисов, обеспечиваемые программами, которые выполняются на других компьютерах; их функциональность доступна через Web-направленный API (эти сервисы требуют специальной техники, такой как протокол SOAP).
  • Вначале обычно достаточно одного облика. Вот почему типичной ошибкой проектирования является построение системы, где модель и облик сложно связаны. Затем, когда понадобятся другие облики, приходится прикладывать массу усилий по перепроектированию системы. Во избежание этого общим принципом должно быть разделение модели и ее обликов уже на начальных этапах проектирования системы.

    Почувствуй методологию

    Принцип разделения модель/облик

    При проектировании архитектуры программной системы сводите к минимуму взаимодействие элементов модели и элементов облика.

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

  • Издатели включают события, которые могут непосредственно изменить облик, обычно в малой мере. Например, курсор может изменить форму при перемещении в другое окно, нажатая кнопка может выглядеть иначе, чем кнопка в обычном состоянии.
  • Подписчики захватывают события (тех типов, на которые они подписались) и обрабатывают их. Обработка может обновить облик.
  • Заметьте, два разделения — издатели-подписчики и модели-облики — взаимно ортогональны. Как издатели, так и подписчики могут взаимодействовать как с моделью, так и с обликами, как это можно видеть на примере системы обработки текстов.

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

    Для проектирования GUI особый интерес представляет схема "Модель — облик — контроллер" (МОК). Роль третьего элемента — контроллера — состоит в управлении интерактивной сессией. Она может включать создание и координацию обликов.

    Каждая из трех частей взаимодействует с двумя другими:

    (рис 7.4) Структура МОК

    Присутствие контроллера обеспечивает дальнейшее разделение между моделью и обликами (помните, что обликов может быть несколько). Контроллер управляет действиями пользователя, которые могут приводить к обновлению модели, облика, или того и другого.

    Как и ранее, облик обеспечивает визуальное представление модели или части ее.

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

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

    Пользуясь случаем, дадим несколько полезных советов, связанных с рисунками. В презентациях ПО часто приводятся выразительные диаграммы с многочисленными блоками, связанными стрелками. Одна беда - недостаточно спецификаций, поясняющих семантику. На нашем рисунке используются для этой цели метки, такие как "представляет", "обновляет" и другие. Неименованные стрелки имеют стандартную семантику, задавая отношения наследования или "клиент-поставщик". Рисунок не хуже многих слов, если только это не просто цветовые эффекты. Не поддавайтесь соблазнам бессмысленной графики - явно задавайте точную семантику используемых символов.

    Страницы:

    Кто в ответе?

    В используемом до сих пор стиле программирования порядок выполнения операций определяла программа. Он следовал из ее собственного сценария, определяемого управляющими структурами: последовательностью, условным оператором и оператором цикла. Внешний мир мог сказать свое слово — через интерфейс пользователя, доступ к базе данных, другой ввод, воздействующий на условия, циклы, динамическое связывание. Но последнее слово оставалось за программой, которая решала, где вычислять эти условия.

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

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

    Такой архитектурный стиль — в конечном счете, еще одна управляющая структура, которую следует добавить к предыдущему каталогу структур, — известна также под именем "издатели-подписчики", метафора, отражающая возможное разделение ролей между элементами ПО.

  • Некоторые элементы, издатели, могут включать события во время выполнения.
  • Некоторые элементы, подписчики, подписываются на определенные типы событий, указывая, какие службы должны вызываться в ответ на возникновение того или иного события.
  • Эти роли не являются исключительными, поскольку некоторые подписчики могут включать собственные события. Заметьте, "событие" является программной концепцией: даже когда события возбуждаются вне ПО — нажатие кнопки мыши, измерение датчика, прибытие сообщения, — для того чтобы быть обработанными, они должны транслироваться в программные события. ПО может включать свои собственные события, не связанные с внешними воздействиями.

    Программирование, управляемое событиями, применимо во многих различных областях. Оно с успехом применяется в графическом интерфейсе пользователя — GUI, что и станет нашим первым примером.

    Управляемое событиями GUI-программирование

    Старый добрый ввод:

    (рис 7.1)

    До появления GUI программы вводили данные, приходящие от некоторого последовательного посредника, например, программа могла читать последовательность строк, обрабатывая каждую из них соответствующим образом:

    from
      read_line
      count : = 0
    until
      exhausted
    loop
      count := count + 1
        — Сохранить last_line в позиции count в Result:
    Result [count] := last_line
      read_line
    end
    

    Здесь read_line пытается читать следующую строку ввода, сохраняя ее в last_line, а значение exhausted принимает значение true, если при выполнении read_line нет больше строк для ввода.

    При такой схеме управление остается за программой : она решает, когда ей нужен очередной ввод данных. Остальной мир — здесь файл или пользователь, печатающий строки на терминале, — должен обеспечить затребованный программой ввод.

    Современный интерфейс

    Добро пожаловать в современный мир. Если программа использует GUI, то пользователю на каждом шаге предоставляется выбор, что же он хочет делать, включая выбор, не связанный с программой, поскольку он может переключиться на работу в другом окне с другой программой, например, послать письмо по электронной почте.

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

    Ожидается, что пользователь выполняет некоторые действия по вводу данных, а наша программа соответствующим образом реагирует на них. Действиями могут быть печать текста в текстовом окне, щелчок по кнопке, выбор пункта меню.

    Но какое действие будет первым, и случится ли вообще хоть что-нибудь?

    (рис 7.2) Интерфейс программы

    Мы не знаем.

    Конечно, мы могли бы использовать большой оператор if... then ... elseif... end или конструкцию выбора со многими ветвями, перечисляя все возможности:

    inspect
      user_action
    when "Нажата кнопка Stop" then
      "Завершить выполнение"
    when "Введен текст в поле Class Name" then
    "В соответствующее окно (слева вверху) вывести текст класса, имя которого
    задано в текстовом поле"
    when    …Другие ветви…
    end
    

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

    Проектирование, управляемое событиями ("издатели-подписчики"), предлагает в такой ситуации совсем другую схему.

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

    (рис 7.3) Включение и обработка событий

    Стиль "Издатели-подписчики" полезен в различных областях приложения: GUI-программирование — это просто один из примеров. Другие примеры:

  • сети передачи данных, где возможна широковещательная передача, — один передатчик и много приемников;
  • управление процессом. Сюда входят программные системы, управляющие производственными процессами. В таких системах данные поступают от многочисленных датчиков, следящих за температурой, давлением и другими характеристиками процесса. Возникающие в результате события обрабатываются подготовленными элементами ПО.
  • Терминология

    Для рассматриваемого стиля программирования важно тщательно определить концепции, обращая, как обычно, внимание на различение типов и их экземпляров. События, издатели и подписчики

    Определение: событие

    Событие - это операция периода выполнения, производимая элементом ПО. В результате события некоторая информация становится доступной для потенциального использования другими элементами ПО, не указанными в операции.

    Это определение высвечивает отличительные свойства событий.

  • Событие сопровождается некоторой информацией: при щелчке мыши должна указываться позиция курсора, при изменении температуры — старая и новая температуры.
  • Всегда включается информация о том, какое событие произошло:
  • 5-го августа 1492 года Христфор Колумб отправился в плавание;
  • 5 минут назад (менее известное событие) я щелкнул левую кнопку моей мыши.
  • Обычно информации больше — куда отправился Колумб, каковы координаты мыши. Но иногда достаточно знать, что событие произошло, например, что истек срок ожидания.
  • Определенные элементы ПО могут использовать эту информацию.
  • Во всех случаях важно то, что само событие не знает своих получателей (писатель не знает читателей). В противном случае этот механизм не отличался бы от механизма вызова методов, подобно вызову x.f (a, b, c), который удовлетворяет всем другим свойствам определения — поставляет информацию (a, b, c), доступную элементу ПО (методу f ). Но когда вызывается метод, то явно указывается адресат вызова. При вызове событий все не так — информация посылается в межадресное пространство, где любой элемент может использовать ее в интересах своей работы.
  • Помните, что для наших целей событие — это операция ПО. Могут существовать внешние события — действия пользователя и прочее, — приводящие к возникновению программных событий, но и сама программа может создавать события в процессе работы.

    Рассмотрим связанную с этими понятиями терминологию, неформально уже встречавшуюся.

    Определения: включение, публикация, издатель, подписчик

    Включением (или публикацией) события является его выполнение. Элемент ПО, включающий событие, - издатель. Элемент ПО, использующий информацию события, - подписчик.

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

    В литературе встречаются синонимы приведенных выше терминов: подписчики называются "наблюдателями", отсюда образец "Наблюдатель", изучаемый в этой лекции. Говорят также о "слушателях" события. Для "издателей" также используется синоним, вытекающий из подобной риторики — "предмет наблюдения" .

    Аргументы и типы событий

    Нам необходимо имя для информации, приходящей — в соответствии с определением — с любым событием.

    Определение: аргумент

    Информация, связанная с событием, состоит из аргументов события.

    Термин "аргумент" указывает на сходство с аргументами метода. Продолжая это сходство, будем предполагать, что аргументы сгруппированы в упорядоченный список, подобно аргументам в вызове x.f (a, b, c) . Как и для методов, список может быть пустым, например, в случае события, указывающего на истечение срока ожидания.

    Как подписчики обнаруживают, что произошло интересующее их событие? Одна модель — опрос (повторяющаяся проверка). Это напоминает ситуацию с подписчиком журнала, который в день издания ходит к почтовому ящику, чтобы проверить, не пришел ли журнал. Вторая модель — уведомление: при включении события об этом уведомляются все подписчики.

    Модели для информации, распределенной в сети Интернет, классифицируются как "притяжение" (ожидание пользователей, заинтересованных в информации) и "проталкивание" (рассылка информации пользователям).

    Модель уведомления — более гибкая, и далее будем предполагать именно ее. Она может работать при условии, что подписчики заранее явно проявили свой интерес. Дело обстоит так же как с подписчиками журнала: на журнал надо заранее подписаться. Но на что можно подписаться? На событие подписаться нельзя, поскольку, по определению, это операция, выполняемая в ходе работы программы: до его появления оно не существует, а после — уже поздно.

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

    Все события одного типа имеют один и тот же список аргументов. Например, список аргументов для события — щелчок левой кнопки мыши — включает координаты мыши в момент щелчка — два целых числа. Во многом концепция заимствована у методов, обладающих сигнатурой — списком типов аргументов. Процедура print (v: VALUE; f: FORMAT) имеет сигнатуру [VALUE, FORMAT] — список типов. Это понятие расширяется на типы события.

    Определения: тип события, сигнатура

    Любое событие принадлежит определенному типу события.

    Все события данного типа события имеют одну и ту же сигнатуру. Например:

  • сигнатура события "изменение температуры" может быть [REAL, REAL], чтобы представлять старое и новое значения;
  • событие "левый щелчок мыши" может иметь сигнатуру [INTEGER, INTEGER] .
  • Можно также взять событие "одиночный щелчок мыши" с третьим компонентом сигнатуры, указывающим, какая кнопка была нажата. В библиотеке EiffelVision применяется вариант, в котором добавлен еще один аргумент, показывающий, остается ли нажатой кнопка, - он полезен (особенно в играх) для джойстика и экзотичных устройств указателей;

  • хотя можно было бы определить тип события для каждой клавиши на клавиатуре, более удобно использовать один тип события "клавиша нажата" с сигнатурой [CHARACTER], где аргумент задает код клавиши;
  • для событий без аргументов, например, "исчерпано время ожидания", сигнатура пуста, как у методов без аргументов.
  • Всякий раз, когда издатель включает событие, он должен обеспечить значение каждого аргумента (если они есть): координаты мыши, код клавиши, температуры. И здесь наблюдается полная аналогия с вызовом метода, где при каждом вызове задаются фактические аргументы.

    Термин "тип события" может предполагать еще одну аналогию, где каждый тип соответствует типам ОО-программирования (классам с возможно родовыми параметрами), а каждое событие соответствует экземпляру класса (объектам). Но сравнение типов событий с программами - более показательное, тогда появление события данного типа соответствует вызову программы.

    При таком подходе событие — это не объект, а тип события — это не класс. Вместо этого можно ввести общее понятие для всех типов событий: класс, называемый ниже EVENT_TYPE, а конкретный тип события, например, "щелчок левой кнопки мыши" (абстракция левых щелчков — вроде того, что я в прошлый понедельник, будучи в расстроенных чувствах, щелкнул в ответ на запрос "Delete all?" ) рассматривать как объект. Как всегда, когда вы раздумываете, не ввести ли класс, критерием является "Можно ли эту абстракцию данных наполнить смыслом, определив множество хорошо понимаемых операций, применимых ко всем объектам класса?". В данном случае:

  • если бы мы решили создавать класс для типа события, его экземплярами были бы события одного типа, но у них не было бы полезных компонентов. Более точно, события имели бы собственные данные — аргументы, но они нуждались бы только в запросах для получения доступа к этим аргументам; команд здесь не было бы;
  • в противоположность такому подходу: если рассматривать конкретный тип события как объект, то появляется несколько хорошо определенных команд и запросов — включить событие с заданным набором аргументов, подписать данного подписчика на этот тип события, удалить подписчика из списка подписчиков, перечислить всех подписчиков, подсчитать число включений события данного типа и так далее. Это богатое множество компонентов характеризует полезный законный класс.
  • Не рассматривать каждое событие как объект полезно и с позиций производительности. Обычно во время выполнения создается большое число событий - каждое малое перемещение курсора включает событие, так что следует избегать создания всех соответствующих объектов. Это не освобождает нас от нагрузки, поскольку аргументы каждого события, представленные кортежем, должны быть записаны. В хорошей библиотеке GUI производительность улучшается за счет того, что для последовательности близких событий можно использовать один кортеж вместо десятков сотен.

    Полезно иметь термины для действий подписчиков с типами событий и событиями.

    Определения: подписать, зарегистрировать, обработать, захватить

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

    Когда элемент получает уведомление о событии, на которое он подписан, он обрабатывает (или захватывает ) событие, выполняя зарегистрированное действие.

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

    При регистрации подписчик задает некоторое действие, выполняемое в ответ на возникновение события данного типа. У действия должен быть способ получения аргументов события. Очевидный путь достижения цели — это указание при регистрации метода, чья сигнатура соответствует сигнатуре типа события. Тогда возникновение события данного типа будет причиной вызова метода, обрабатывающего событие, где аргументы события играют роль фактических аргументов вызова.

    Теперь у нас есть полная картина того, как работает программа, управляемая событиями.

    Схема управления событиями

    E1 Некоторые элементы, издатели, позволяют остальной системе узнать, какие типы событий они могут включать.

    E2 Некоторые элементы, подписчики, заинтересованы в обработке событий определенных типов. Они регистрируют соответствующие действия.

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

    В примере GUI:

    E1 Издателем является некоторый элемент ПО, который следит за устройствами ввода и включает события при определенных обстоятельствах, например, при нажатии клавиш клавиатуры или кнопок мыши. Обычно нет необходимости писать такое ПО, поскольку оно является частью библиотеки GUI - EiffelVision для Eiffel, Windows Forms для .NET, Swing для Java.

    E2 Подписчиком является любой элемент, которому требуется обработать GUI-события. Он регистрирует методы, выполняемые в ответ на события. Например, метод, сохраняющий файл, можно зарегистрировать для события щелчок мыши на кнопке с надписью "OK" в диалоге по сохранению файла.

    E3 Если во время выполнения пользователь щелкнет по кнопке OK, то это станет причиной выполнения метода — или методов, — зарегистрированных для данного типа события.

    Важное свойство этой схемы, проиллюстрированное на последнем рисунке: выделение двух сторон, участвующих в создании и обработке события, — подписчики и издатели ничего не знают друг о друге. Более точно, определение "события" требует, чтобы подписчики не знали других подписчиков. Остальное — дело методологии, и мы увидим, как различные архитектурные решения достигают результата по отношению к этому критерию.

    Ясное понимание различий

    Возможно, вы полагаете, что различие между типом события и самим событием очевидно, но в литературе эти понятия зачастую путаются, из-за чего простые вещи кажутся сложными. Это предупреждение должно помочь вам при изучении различных механизмов программирования, управляемого событиями.

    Следующий текст представляет часть документации .NET от Microsoft, представляющей обработку событий, чьи концепции отражены в языках C# и Visual Basic .NET

    Обзор событий

    События имеют следующие свойства.
  • Издатель определяет, когда событие включается; подписчики определяют, какие действия будут выполняться в ответ на событие.
  • Событие может иметь много подписчиков. Подписчик может обрабатывать события, приходящие от многих издателей.
  • События, не имеющие подписчиков, никогда не вызываются.
  • События широко используются для сигнализации о действиях пользователя, таких как щелчки по кнопкам или выбор в меню в графическом интерфейсе пользователя.
  • При наличии множества подписчиков вызов обработчиков события синхронизируется. Для асинхронного вызова смотри следующий раздел.
  • События могут использоваться для синхронизации потоков.
  • События в библиотеке классов .NET Framework основаны на делегате EventHandler и базовом классе EventArgs .
  • Здесь один и тот же термин "событие" в разных контекстах имеет разный смысл: иногда речь идет действительно о событиях, иногда о типах событий, иногда об обоих понятиях одновременно. Думаю, что вы согласитесь с моей интерпретацией. В частности:

  • невозможно подписываться на события (пункты 1, 5). Как мы видели, событие не существует, пока оно не будет возбуждено. Подписчик подписывается на тип события, уведомляя, что он хочет получать уведомление о каждом событии этого типа, когда оно произойдет во время выполнения;
  • в пункте 7 говорится о свойствах классов, описывающих типы событий. Заметьте, что в .NET каждый тип события объявляется как класс. Такой класс должен наследовать от класса EventHandler, представляющего классы, которые называются делегатами (delegate) — последние обеспечивают механизм, подобный агентам. Еще один класс — EventArgs — является родительским классом для классов, задающих аргументы события;
  • пункт 3 звучит загадочно, пока не осознаешь, что имеется в виду следующее: "Если тип события не имеет подписчиков, включение события этого типа не дает никакого эффекта". Все это описывает внутреннюю оптимизацию: обнаружив, что тип события не имеет подписчиков, механизм события удаляет избыточно возбужденные события, что в .NET влечет к созданию объекта для каждого события.
  • Возможность непонимания особенно ярко проявляется в двух местах.

  • В пункте 2 говорится: "Подписчик может обрабатывать множественные события от множественных издателей". Из этого комментария можно сделать вывод, что речь идет о сложной параллельной схеме вычислений, где подписчик захватывает события из различных мест одновременно, но реально это просто означает, что подписчик может регистрировать несколько типов событий и каждый издатель может включать события разных типов.
  • В пункте 5 устанавливается, что для события, имеющего множество подписчиков, вызов обработчиков события синхронизируется. Предложение просто пытается сказать, что когда множество подписчиков зарегистрировали один и тот же тип события, они обрабатывают соответствующие события синхронно.
  • Так что при изучении схем управления событий проверяйте, о чем идет речь — о событиях или о типах событий, и убедитесь (это одно из наших очередных наставлений), что в вашей документации по событиям используется правильная терминология.

    Контексты

    Подписчик при регистрации говорит: "Для события этого типа выполняй это действие". На практике может быть полезно, особенно для приложений GUI, уточнить высказывание: "Для события этого типа, встречающегося в данном контексте, выполняй это действие". Например:

  • "если пользователь нажал левую кнопку мыши на кнопке OK, сохрани файл";
  • "если мышь находится в этом окне, измени цвет границ на красный";
  • "если датчик показывает температуру выше 25° С, включи сигнал тревоги".
  • В первом случае "контекстом" является элемент управления, а событием — "щелчок мыши". Во втором контекст — окно, событие — появление мыши; в третьем контекст — датчик, событие — превышение режима.

    Для GUI-программирования контекстом обычно является элемент управления. Как показывает последний пример, понятие контекста более общее; контекст может быть любым условием с булевским значением. Примеры GUI являются специальным случаем, где булевское условие задает свойство, такое как "курсор установлен на этой кнопке" или "курсор находится в этом окне". Вот общее определение:

    Определение: контекст

    При управлении событиями контекст представляет булевское выражение, задаваемое подписчиком в момент регистрации, но вычисляемое в момент включения, так что регистрируемое действие будет выполняться только, когда контекст принимает значение True.

    Понятие контекста нам знакомо по обычному стилю программирования, не связанному с событиями: вспомните итератор, такой как do_if, который выполняет действия над всеми элементами структуры, удовлетворяющими некоторому условию.

    Это аналогично тому, как контекст позволяет подписчику установить, что его интересуют события данного типа, но необходимо, чтобы в момент включения выполнялось определенное условие.

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

    if "Курсор на значке Exit" then
      "Выполнить предусмотренные действия"
    end
    

    Удобнее отделять условие, задавая его вместе с типом события и действием.

    Требования "Публиковать-подписаться"

    Установив концепции, будем теперь заниматься поиском общего решения проблемы, проектируя архитектуру управления событиями. Начнем с ограничений, которым должно удовлетворять хорошее решение.

    Издатели и подписчики

    При проектировании архитектуры, поддерживающей парадигму "Публиковать-подписаться", следует рассмотреть следующие ограничения.

  • Издатели не должны знать, кто является подписчиком. Они включают события, но в соответствии с определением события не знают, кто может их обрабатывать. Типичным случаем издателя является библиотека GUI. Методы библиотеки знают, как обнаружить событие, инициированное пользователем, такое как "щелчок кнопки", но они ничего не должны знать о конкретных приложениях, реагирующих на эти события, и о том, как они реагируют. В приложении нажатие кнопки может сигнализировать о начале компиляции, запуске процесса оплаты, выключении завода или запуске ракеты. Но все это для библиотеки GUI — просто щелчок кнопки.
  • Любое событие, включаемое одним издателем, может поставляться нескольким подписчикам. Изменение температуры в системе управления завода может отражаться в местах, "наблюдающих" за этим типом события. Данные могут отражаться на обычном и на графическом дисплее, в службе безопасности, включающей определенные действия при нарушениях режима, в записях изменений в базе данных.
  • Подписчикам нет необходимости знать издателей. Это более строгое, но часто желательное требование. Подписчики знают о типах событий, на которые они подписываются, но не должны знать, откуда приходят события. Помните, что одна из целей проектирования, управляемого событиями, состоит в обеспечении гибкой архитектуры, позволяющей включать разных издателей и разных подписчиков, возможно, написанных разными людьми и в разные времена.
  • Желательно иметь возможность во время выполнения как проводить регистрацию, так и отменять ее. В обычной схеме регистрация выполняется в фазе инициализации приложения, где устанавливаются его параметры до начала "настоящего" выполнения. Но это не является обязательным требованием — гибкость может быть полезной.
  • Должно быть возможным задавать события, зависящие или не зависящие от контекста. Мы видели полезность связывания события с контекстом, но решение должно обеспечивать возможность не придумывать искусственный контекст и просто подписаться на событие независимо от того, где оно случилось.
  • Связывание издателей и подписчиков должно выполняться с минимальными усилиями. Схему с событиями часто требуется добавить в уже существующее приложение. Для связывания сторон требуется добавить некоторый код, часто называемый "склеивающим кодом": чем меньше клея — тем лучше.
  • Последнее требование является критическим для качества системной архитектуры, особенно когда целью является построение пользовательских интерфейсов: не должно быть так, чтобы проектирование ядра зависело от особенностей интерфейса. Это наблюдение непосредственно приводит к нашим следующим понятиям — модели и облику.

    Модель и облик

    При проектировании интерфейса мы не только не должны различать подписчиков и издателей, но и различать два дополняющих аспекта приложения.

    Определения: модель и облик программной системы

    Модель (называемая также бизнес-моделью ) является той частью программной системы, которая обрабатывает данные, представляющие информацию прикладной области.

    Облик - это представление части этой информации при взаимодействии системы с внешним окружением: человеком - пользователем системы, материальными устройствами, другим ПО.

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

    Модель является частью ПО — частью, имеющей дело с прикладной областью. Для платежной системы это та часть, которая обрабатывает информацию о служащих, их часах работы, начисляет зарплату, обновляет базу данных. Для системы управления полетом — прокладывает маршрут, вычисляет времена, авторизацию и прочее. Можно сказать, что модель — это часть, выполняющая "настоящую" работу, независимо от взаимодействия с пользователями ПО и остальным миром.

    Понятие "бизнес-модель" является более точным, но мы обычно предпочитаем говорить просто "модель". Одна из причин в том, что термин "бизнес" порождает неверные ассоциации (управление компанией, финансами), исключая такие области, как обработка текстов или управление полетами.

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

    Обычно программа предназначена для одной — возможно, весьма широкой — прикладной области, но обликов у программы может быть несколько. Хорошей практикой является рассмотрение программы с разных точек зрения. При наивном проектировании небольших программ не уделяется должного внимания этой проблеме. Но для серьезных систем необходимо планировать несколько обликов, таких как:

  • GUI-облик (обычно несколько);
  • Web-облик ("WUI"), позволяющий использовать систему через Web-браузер;
  • текстовый интерфейс — для ситуаций, когда графическая поддержка не нужна или невозможна;
  • интерфейс, основанный на batch-файлах, где подготовлен сценарий, заготовлены исходные данные и вывод пишется в заданное место. Это особенно полезно для интерактивных систем. Интерактивное тестирование трудно, оно требует присутствия людей, проводящих долгие сессии, пытаясь проверить различные комбинации. Вместо этого можно подготовить коллекцию сценариев (обычно записываемых во время сессии с участием человека), а затем выполнять их без участия человека;
  • облики, обеспечиваемые другими программами, которые выполняются локально; их функциональность доступна через API;
  • облики Web-сервисов, обеспечиваемые программами, которые выполняются на других компьютерах; их функциональность доступна через Web-направленный API (эти сервисы требуют специальной техники, такой как протокол SOAP).
  • Вначале обычно достаточно одного облика. Вот почему типичной ошибкой проектирования является построение системы, где модель и облик сложно связаны. Затем, когда понадобятся другие облики, приходится прикладывать массу усилий по перепроектированию системы. Во избежание этого общим принципом должно быть разделение модели и ее обликов уже на начальных этапах проектирования системы.

    Почувствуй методологию

    Принцип разделения модель/облик

    При проектировании архитектуры программной системы сводите к минимуму взаимодействие элементов модели и элементов облика.

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

  • Издатели включают события, которые могут непосредственно изменить облик, обычно в малой мере. Например, курсор может изменить форму при перемещении в другое окно, нажатая кнопка может выглядеть иначе, чем кнопка в обычном состоянии.
  • Подписчики захватывают события (тех типов, на которые они подписались) и обрабатывают их. Обработка может обновить облик.
  • Заметьте, два разделения — издатели-подписчики и модели-облики — взаимно ортогональны. Как издатели, так и подписчики могут взаимодействовать как с моделью, так и с обликами, как это можно видеть на примере системы обработки текстов.

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

    Для проектирования GUI особый интерес представляет схема "Модель — облик — контроллер" (МОК). Роль третьего элемента — контроллера — состоит в управлении интерактивной сессией. Она может включать создание и координацию обликов.

    Каждая из трех частей взаимодействует с двумя другими:

    (рис 7.4) Структура МОК

    Присутствие контроллера обеспечивает дальнейшее разделение между моделью и обликами (помните, что обликов может быть несколько). Контроллер управляет действиями пользователя, которые могут приводить к обновлению модели, облика, или того и другого.

    Как и ранее, облик обеспечивает визуальное представление модели или части ее.

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

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

    Пользуясь случаем, дадим несколько полезных советов, связанных с рисунками. В презентациях ПО часто приводятся выразительные диаграммы с многочисленными блоками, связанными стрелками. Одна беда - недостаточно спецификаций, поясняющих семантику. На нашем рисунке используются для этой цели метки, такие как "представляет", "обновляет" и другие. Неименованные стрелки имеют стандартную семантику, задавая отношения наследования или "клиент-поставщик". Рисунок не хуже многих слов, если только это не просто цветовые эффекты. Не поддавайтесь соблазнам бессмысленной графики - явно задавайте точную семантику используемых символов.

    Вернуться к учебному плану