Опубликован: 04.04.2012 | Доступ: свободный | Студентов: 1989 / 61 | Оценка: 4.60 / 4.40 | Длительность: 13:49:00
Лекция 9:

Образцы проектирования

< Лекция 8 || Лекция 9: 12345 || Лекция 10 >
Аннотация: В лекции рассматриваются образцы проектирования, связанные с событиями. Подробно рассматривается образец "Наблюдатель" (pattern Observer). Обсуждаются достоинства и недостатки этого классического образца. Используя такие возможности языка Eiffel, как агенты и кортежи, рассматривается более совершенный образец, применимый в задачах типа "Издатели" и "Подписчики", - образец, основанный на классе EVENT_TYPE. Этот класс является основой контроллера при построении более мощного образца проектирования МОК – Модель - Облик – Контроллер (MVC – Model – View – Controller).

Образец "Наблюдатель"

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

Об образцах проектирования

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

Десятки таких образцов — "Наблюдатель" один из них — документированы и широко изучены.

Основы образца "Наблюдатель"

В качестве общего решения управления событиями "Наблюдатель" не вполне хорош — его ограничения будут проанализированы. Но его следует знать по ряду причин.

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

На следующем рисунке приведена типичная архитектура "Наблюдателя". Два общецелевых класса PUBLISHER и SUBSCRIBER не несут специфики конкретного приложения; PUBi и SUBj используются для представления классов типичного издателя и подписчика в вашем приложении:

 Архитектура образца Наблюдатель

Рис. 8.1. Архитектура образца Наблюдатель

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

Как отмечалось, подписчики наблюдают (отсюда имя образца) за издателями, ожидая от них сообщений. Издатели являются субъектом наблюдения. В литературе по образцам используются различные синонимы применяемых нами терминов.

На стороне издателя

Класс PUBLISHER определяет свойства типичного издателя, отвечающего за тип события. Процедура publish этого класса позволяет включать события этого типа. Главной структурой данных является список подписчиков .

note
  what: Наблюдаемые подписчиками объекты, публикующие события одного типа
class
  PUBLISHER
feature {SUBSCRIBER} — Status report
  subscribed (s : SUBSCRIBER): BOOLEAN
      — Является ли s подписчиком данного издателя?
    do
      Result := subscribers.has (s)
    ensure
      present: has (s)
    end
feature {SUBSCRIBER} — Element change
  subscribe ( s : SUBSCRIBER)
      - Сделать s подписчиком данного издателя.
    do
      subscribers.extend (s)
    ensure
      present: subscribed ( s)
    end
  unsubscribe ( s : SUBSCRIBER)
      - Удалить s из списка подписчиков этого издателя.
    do
      subscribers.remove_all_occurrences (s)
    ensure
      absent: not subscribed ( s)
    end
  publish (args : LIST [ANY ])      —Схема аргументов 1
      - Опубликовать событие для подписчиков.
    do
      ... Смотри ниже ...
    end
feature {NONE} —Реализация
  subscribers: LINKED_LIST [SUBSCRIBER]
      - Подписчики, подписанные на событие этого издателя.
end

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

Реализация позволяет дважды вызывать subscribe для того же самого подписчика, тогда (смотри publish ниже) подписчик будет дважды выполнять предписанные действия для каждого события.

В большинстве случаев такой эффект не желателен. Во избежание этого можно было бы обернуть тело subscribe в if not subscribed (s) then ... end, но тогда терялась бы эффективность связного списка, так как требовался бы его обход. Для нашего обсуждения это не критично, но должно учитываться при любом практическом использовании образца; эта проблема является предметом упражнения в конце этой лекции.

Кроме атрибута subscribers, спроектированного для внутренних целей и, следовательно, закрытого (экспортируемого NONE ), остальные компоненты предназначены только для подписчиков, но не для объектов других типов, — по этой причине они экспортируются только классу SUBSCRIBER (как вы помните, это означает, что они экспортируются и потомкам класса, которым необходима возможность подписаться или отменить подписку).

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

На стороне подписчика

note
  what: "Регистрируемые объекты, обрабатывающие события данного типа"
deferred class
  SUBSCRIBER
feature — Element change
  subscribe (p: PUBLISHER)
        — Подписаться у издателя p.
    do
      p.subscribe (Current)
    ensure
      present: p.subscribed (Current)
    end
  unsubscribe ( p: PUBLISHER)
        - Убедиться, что этот подписчик не подписан у p.
    do
      p.unsubscribe ( Current)
    ensure
      absent: not p.subscribed (Current)
    end
feature {NONE} — Basic operations
    handle (args: LIST [ANY])      —Схема аргументов 1
        - Реакция на публикацию события подписанного типа
    deferredend
    end
end

О схеме аргументов будет сказано ниже.

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

Чтобы подписаться на тип события у соответствующего издателя p, подписчик выполняет subscribe (p) . Заметьте, как эта процедура (и аналогично unsubscribe ) использует соответствующий компонент от PUBLISHER, передавая ему для подписки текущий объект. Это еще одна из причин выборочного экспорта в классе PUBLISHER — было бы бесполезно для класса подписчика применять subscribe от PUBLISHER непосредственно. Подписка имеет смысл, только если обеспечивается соответствующий механизм обработки handle, приходящий от класса SUBSCRIBER (этим объясняется и использование общих имен компонентов в двух классах, что сохраняет терминологию простой и не приводит к недоразумениям, так как компоненты экспортируются только подписчикам).

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

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

Один неприятный момент: необходимо убедиться, что операция получает аргументы правильных типов. Причина в том, что мы пытаемся сделать PUBLISHER и SUBSCRIBER общими, а потому должны объявить аргументы args, представляющие аргументы события как в publish, так и в handle полностью общего типа: LIST [ANY] . Но тогда handle должна выполнять кастинг, преобразуя args к правильному типу и числу аргументов.

Предположим, например, что тип события объявляет два аргумента соответствующих типов T и U . Мы хотим обработать каждое событие, вызывая метод op (x: T;y: U) . Тогда следует написать handle следующим образом:

handle (args: LIST [ANY ])    — Схема Аргументов 1
  — Выполнение операции op над аргументами в ответ на публикацию события.
    do
      if args.count >= 2 and then
        (attached {T } args.item (1) as x) and
        (attached {U } args.item (2) as y)
      then
        op ( x, y)
      else
        — Не делать ничего или выдать отчет об ошибке
      end
    end

Тест объектов позволит убедиться, что первый и второй элементы списка args имеют ожидаемые типы и свяжет их с x и y внутри then ветви

Единственный способ избежать такого тестирования, выполняемого в период выполнения, состоит в специализации PUBLISHER и SUBSCRIBER, объявив publish и subscribe с точными типами аргументов, например:

publish (x: T ; y : U)    — Схема аргументов 2

Аналогично для handle в SUBSCRIBER . В этом случае теряется общность схемы, поскольку нельзя использовать классы PUBLISHER и SUBSCRIBER для типов событий с различающимися сигнатурами. Хотя отчасти это дело вкуса, но я рекомендовал бы схему аргументов 2 в случае применения образца "Наблюдатель", поскольку в этом случае ошибки — издатель передал неверные аргументы — будут обнаруживаться на этапе компиляции.

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

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

Можно построить решение, безопасное по типам, сделав классы PUBLISHER и SUBSCRIBER универсальными. Родовым параметром является кортежный тип, представляющий сигнатуру типа события (другими словами - последовательность типов аргументов). Подобное решение появится ниже в заключительной архитектуре "публиковать-подписываться" ("Event Library"). Мы не станем разрабатывать его для образца "Наблюдатель", поскольку оно основано на механизмах - типы кортежей, ограниченная универсальность, - недоступных в других языках. Если же вы программируете на Eiffel, то следует использовать заключительную архитектуру, которая лучше образца "Наблюдатель" и доступна в библиотеке классов Eiffel. Однако это хорошее упражнение - улучшить "Наблюдатель", используя эти идеи. Попытайтесь это сделать прямо сейчас, не дожидаясь появления решения на последующих страницах.

Публикация события

Единственная пропущенная часть реализации образца "Наблюдатель" — это тело процедуры publish в PUBLISHER, хотя, я надеюсь, мысленно вы ее уже написали. Вот где образец выглядит особенно элегантным:

publish (args: ... Схема аргументов 1 или 2, смотри обсуждение выше .,.)
    - Публиковать событие для подписчиков.
  do
    - Просить каждого из подписчиков в свою очередь
    - обработать послание:
    from subscribers.start until subscribers.after loop
      subscribers.item.handle ( args)
      subscribers.forth
    end
  end

Для схемы аргументов 1 args принадлежат типу LIST [ANY] . Для схемы аргументов 2 объявление специфицирует точно ожидаемые типы.

Операторы программы показывают преимущества полиморфизма и динамического связывания: subscribers — это полиморфный контейнер, каждый элемент списка может быть разного SUBSCRIBER -типа, характеризуемый своим вариантом обработки события handle . Динамическое связывание гарантирует, что в каждом случае будет вызываться правильный вариант. Это просто праздник лучших приемов ОО-архитектуры!

< Лекция 8 || Лекция 9: 12345 || Лекция 10 >
Надежда Александрова
Надежда Александрова

Уточните пожалуйста, какие документы для этого необходимо предоставить с моей стороны. Курс "Объектно-ориентированное программирование и программная инженения".