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

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

< Лекция 8 || Лекция 9: 12345 || Лекция 10 >

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

Образец "Наблюдатель" широко известен и используется. Это интересное применение ОО-приемов. Как общее решение проблемы "публиковать-подписаться", он страдает рядом ограничений.

  • Как обсуждалось, возникает неприятная дилемма выбора между двумя схемами аргументов, одинаково непривлекательными: с одной стороны, опасная проверка типов аргументов в момент выполнения, чреватая ошибками, с другой — специфические, квазиидентичные классы PUBLISHER и SUBSCRIBER, задающие сигнатуру для каждого типа события.
  • Подписчики непосредственно подписываются у издателей. Это и есть причина нежелательной связи между двумя сторонами. Подписчики не должны знать, какая часть приложения или какая библиотека включает события. Фактически, пропущен еще один участник процесса — брокер — посредник между двумя сторонами. Более фундаментальная причина состоит в том, что пропущена ключевая абстракция — тип события, которая в образце сливается с понятием издателя.
  • С единственным общецелевым классом PUBLISHER подписчик может регистрироваться только у одного издателя, а у этого издателя он может зарегистрировать только одно действие, представленное handle ; как следствие, он может подписаться только на один тип события. Это серьезное ограничение. Компонент приложения должен быть способным регистрировать различные операции у различных издателей. С этой проблемой можно было бы справиться, добавляя в методы publish и handle аргумент, представляющий издателя, так, чтобы подписчики могли выбирать издателя. Но такое решение губительно с позиций модульного проектирования, так как теперь процедуры обработки должны будут знать обо всех событиях. Еще один возможный прием — иметь несколько независимых издательских классов, по одному на каждый тип события. Это решает проблему, но в жертву приносится повторное использование.
  • Поскольку издательские классы и классы подписчиков должны наследоваться от PUBLISHER и SUBSCRIBER, то непросто связать существующую модель с новым обликом без добавления существенного слоя склеивающего кода. В частности, невозможно непосредственное повторное использование существующей процедуры из модели (op в нашем примере) как действие, регистрируемое подписчиком. Причина в том, что в реализации handle эта процедура должна вызываться с аргументами, заданными издателем.
  • Предыдущая проблема усугубляется в языках без множественного наследования. Издательские классы и классы подписчики наследуют от классов PUBLISHER и SUBSCRIBER, реализуя отложенные методы родителей соответственно publish с его фундаментальным алгоритмом и subscribe . Но этим классам в соответствии с их ролью в модели могут требоваться и другие родители. Единственное решение — писать больше склеивающего кода и делать эти классы клиентами соответствующих классов модели.
  • Наконец, заметьте, что классы, приведенные выше, корректны по отношению к некоторой проблеме, возникающей при приводимой в литературе стандартной реализации образца. Например, обычная презентация образца "Наблюдатель" связывает подписчика и издателя в момент создания, используя издателя как аргумент процедуры создания подписчика. Вместо этого в приведенной выше реализации предусмотрена специальная процедура подписки subscribe в классе SUBSCRIBER, позволяющая связать наблюдателя с издателем в любой момент по желанию. Более того, можно отсоединиться от издателя, а позже снова с ним соединиться.
  • Все эти проблемы не мешают проектировщикам успешно использовать образец в течение многих лет, но приводят к двум серьезным последствиям. Во-первых, в строящихся решениях отсутствует нужная гибкость, что является причиной дополнительной работы, например, написания склеивающего кода, присутствие связей между элементами ПО, в которой нет необходимости, что всегда плохо с позиций эволюции системы. Во-вторых, отсутствует повторное использование, так что каждый программист должен строить реализацию образца в своих интересах.

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

Использование агентов: библиотека EVENT

Теперь мы собираемся показать, как, придав понятию типа события его полную роль, можно получить решение, устраняющее все упомянутые ограничения. Решение не только более гибкое, чем то, что мы видели до сих пор, — оно полностью повторно используемое (через библиотеку классов, которую можно использовать в качестве единственной основы API); более того, оно намного проще. Ключом являются механизмы агентов и кортежей.

Базисный API

Сосредоточимся на основной абстракции данных, следующей из обсуждения в начале этой лекции, — типе события. У нас больше не будет двух замечательных классов PUBLISHER и SUBSCRIBER — да-да, единственный класс решает всю проблему, класс, называемыйEVENT_TYPE.

В основе: два компонента характеризуют тип события.

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

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

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

Этих наблюдений достаточно для определения интерфейса класса:

note
  what: " Типы событий, позволяющие публикацию и подписку "
class EVENT_TYPE [ARGUMENTS -> TUPLE] feature
  publish (args: ARGUMENTS)
    - Включение события этого типа.
  subscribe (action: PROCEDURE [ANY, ARGUMENTS])
    - Регистрация действия, которое должно быть выполнено
    - для события этого типа.
  unsubscribe ( action: PROCEDURE [ ANY, ARGUMENTS])
    - Отмена регистрации действия (подписки) на события этого типа.
end

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

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

Использование типов событий

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

left_click: EVENT_TYPE [TUPLE [x: INTEGER; y: INTEGER]
 — Тип события, представляющий события "щелчок левой кнопки мыши"
    once
      create Result
    end
Листинг 8.1.

Функция left_click возвращает объект, представляющий желаемый тип события.

Помните, что нам не требуется иметь отдельный объект для каждого события, это означало бы пустую трату пространства. Нам нужен только один объект на все события "левый щелчок". Поскольку этот объект должен быть доступен нескольким частям ПО — издателям и подписчикам, — системе требуется только один экземпляр. Эту возможность обеспечивает использование once-метода — метода, выполняемого только один раз, (один из немногих механизмов Eiffel, с которым мы еще не сталкивались в этой книге). Как следует из его имени, метод, помеченный как "once", вместо do или deferred, выполняет тело метода один раз при первом вызове, если таковой существует. Последующие вызовы процедуры не выполняются, а вызовы функций, как в данном случае, каждый раз будут возвращать один и тот же объект, созданный при первом вызове. Одно из преимуществ: не требуется беспокоиться о том, когда создать объект, — любая часть системы, в которой потребовалось первый раз выполнить "левый щелчок", создаст объект.

Вскоре мы увидим, где должно появиться объявление типа события [8.1]. Пока же будем полагать, что классам подписчиков и издательским классам этот объект доступен.

Чтобы включить событие, издатель — например, элемент GUI-библиотеки, который обнаруживает щелчок мыши, — просто вызывает publish на этом объекте с подходящим кортежем аргументов, как в нашем примере:

left_click.publish ([your_x, your_y])

На стороне подписчика также все просто — чтобы подписать действие, представленное процедурой p (x, y: INTEGER), достаточно вызвать на этом объекте метод subscribe :

left_click.subscribe (agent p)
Листинг 8.2.

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

Если нужно иметь единственный тип события, публикуемый для всех потенциальных подписчиков, просто сделайте его доступным как для издателя, так и для всех классов подписчиков, поместив объявление [8.1]в "обслуживающий" класс, к которому они все имеют доступ, например, наследуя от него.

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

your_button.left_click.subscribe (agent p)    
Листинг 8.3.

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

Когда контекст является значимым, тогда подписчики подписываются не просто на тип события, как в [8.2], а на тип, встречающийся в определенном контексте как в [8.8.3]. Подходящим архитектурным решением является объявление релевантных типов событий в соответствующих контекстных классах. Объявление left_click [8.8.1] становится частью класса BUTTON . Оно остается однократной (once) функцией, так как тип события — общий для всех кнопок этого вида. Объект, представляющий тип события, будет создан при первом вызове subscribe или publish . Если "левый щелчок" релевантен нескольким видам графических элементов — кнопкам, окнам, пунктам меню, — то каждый из соответствующих классов будет иметь атрибут, такой как left_click, одного и того же типа. Механизм "once" гарантирует, что существовать будет только один объект для типа события, более точно — один для каждого типа графических элементов.

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

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

В первом случае тип события будет иметь один экземпляр, разделяемый всеми подписчиками. Во втором — будет самое большое по одному объекту на каждый контекстный тип, для которого тип события имеет место.

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

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