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

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

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

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

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

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

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

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

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

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

  • Это некоторая классика.
  • Здесь элегантно используются преимущества ОО-механизмов, таких как полиморфизм и динамическое связывание.
  • Это лучшее, что можно сделать в языках, где нет агентов, универсальности и кортежей.
  • Он дает хорошую основу для перехода к более разумному решению, изучаемому далее.
  • На следующем рисунке приведена типичная архитектура "Наблюдателя". Два общецелевых класса 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 . Динамическое связывание гарантирует, что в каждом случае будет вызываться правильный вариант. Это просто праздник лучших приемов ОО-архитектуры!

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

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

  • Как обсуждалось, возникает неприятная дилемма выбора между двумя схемами аргументов, одинаково непривлекательными: с одной стороны, опасная проверка типов аргументов в момент выполнения, чреватая ошибками, с другой — специфические, квазиидентичные классы 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
    

    Функция 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.1]в "обслуживающий" класс, к которому они все имеют доступ, например, наследуя от него.

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

    your_button.left_click.subscribe (agent p)    
    

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

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

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

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

    Реализация типа события

    Вернемся к рассмотрению сути — знакомству с реализациейEVENT_TYPE. Она подобна приводимой выше реализации PUBLISHER . Закрытый компонент subscribers хранит список подписчиков. Его сигнатура теперь такова:

    subscribers: LINKED_LIST [PROCEDURE [ANY, ARGUMENTS]]
    

    Здесь, как и прежде,LINKED_LIST — наивная структура, но вполне достаточная для нашего обсуждения (лучшую структуру можно увидеть в тексте фактического классаEVENT_TYPE из Event-библиотеки; можно также выполнить упражнение в конце лекции).

    Элементы, хранимые в списке, больше не являются подписчиками, понятие, в котором теперь архитектура решения не нуждается, — это просто агенты. Тип каждого такого агента, PROCEDURE [ANY, ARGUMENTS] указывает, что агент представляет процедуру с аргументами кортежного типа ARGUMENTS, как это определено в классе. Это значительно улучшает безопасность типов решения, в сравнении с тем, что мы видели ранее: несоответствия будут захвачены во время компиляции как плохие аргументы subscribe .

    Метод subscribe (в наивной реализации) может выглядеть так:

    subscribe (action: PROCEDURE [ANY, ARGUMENTS])
        - Зарегистрировать действие, которое должно быть выполнено для
        - событий этого типа.
      do
        subscribers.extend (action)
      ensure
        present: subscribers.has (action)
      end
    

    Использование ARGUMENTS — второго родового параметра класса в типе PROCEDURE — гарантирует, что все процедуры будут отвергнуты, если они не имеют аргументов нужного типа.

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

    publish (args: ARGUMENTS)
          — Публиковать событие подписчикам.
      do
        — Включить событие этого типа.
        from subscribers.start until subscribers.after loop
          subscribers.item.call (args)
          subscribers.forth
        end
    end
    

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

    Только что описанное решение лежит в основе библиотек Event и EiffelVision GUI. Оно широко используется для графических приложений, как небольших, так и сложных, включая само окружение EiffelStudio.

    Класс включает еще несколько деталей, которые стоит внимательно рассмотреть.

    Время чтения программ!

    Типы событий

    Познакомьтесь с текстом библиотечного классаEVENT_TYPE и убедитесь, что вы все в нем понимаете.

    Дисциплина подписчиков

    Если применять любой из приемов этой лекции — от несовершенного образца "Наблюдатель"публиковать-подписываться/codeuot; до механизма, основанного на агентах, — то следует уделить внимание проблемам производительности, которые могут приводить к потенциально разрушительным потерям памяти. Избежать их достаточно просто, если корректно определять поведение подписчиков.

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

    Не забывайте вовремя отменять подписку

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

    Чем вызвано это правило? Проблемой в использовании памяти. Из реализации subscribe, как для версии PUBLISHER в образце "Наблюдатель", так и для версии из классаEVENT_TYPE, следует, что издатель записывает в список подписчиков ссылку на объект подписчика. В приложениях GUI издатель принадлежит облику, а подписчик — модели. Поэтому объект облика сохраняет ссылку на объект модели, который, в свою очередь, может содержать другие ссылки на многие объекты модели (например, самолеты, полеты, расписание и так далее в системе управления полетами).

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

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

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

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

    handler:=agent p
    left_click.subscribe (handler)
        ... — Тогда, когда пришло время отмены подписки:
    left_click.unsubscribe ( handler)
    

    Подписка через переменную, задающую агента, вместо его непосредственного использования, как в предыдущих примерах left_click.subscribe (agent p), гарантирует, что отмена подписки применяется к тому же самому объекту (в отличие от left_click.unsubscribe (agent p), который мог быть применен к новому объекту). Если вы уже подписали данный обработчик более одного раза к данному типу события, отмена (использующая remove_all ) удалит все такие подписки.

    Архитектура ПО. Уроки

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

    Выбирайте правильные абстракции

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

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

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

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

    Вернемся к МОК

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

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

    (рис 8.2) Прямая подписка

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

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

  • Негативная сторона в том, что изменение обликов становится затруднительным. Если не ограничиваться одним обликом, то новые облики должны включать одни и те же события. Это предполагает, что различные облики не полностью различны, например, один из них может задавать интерфейс GUI, а другой — Web.
  • С другой стороны, преимущество в большой простоте. Элементы модели могут непосредственно указать, какие действия следует выполнять для специфических событий, приходящих от интерфейса. Это позволяет не иметь склеивающего кода.
  • Эта схема хороша для относительно простых программ, где интерфейс или, по меньшей мере, стиль интерфейса, известен и стабилен. Для более сложных случаев можно ввести явный контроллер, в задачу которого входит отделение от модели подписки на тип события.

    (рис 8.3) Подписка через контроллер

    Контроллер теперь становится независимой частью ПО. Его работа состоит в подписке элементов модели на типы события. Он будет иметь связи с:

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

    Модель как издатель

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

    Схему можно расширить, позволив модели играть роль издателя. Например, если элемент GUI, такой как "сектор круговой диаграммы", отражает множество значений, которое может изменяться в модели, то модель становится причиной события "change". Облики становятся подписчиками этого типа события.

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

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

    Утром инвестиции, вечером прибыль

    Общим для двух архитектур — "Наблюдатель" и "Библиотека Event" — является необходимость предварительной подписки на тип события до начала их обработки.

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

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

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

    Оценка архитектуры ПО

    Ключом качества программных систем является их архитектура, покрывающая такие аспекты, как:

  • выбор классов, основанных на подходящей абстракции данных;
  • решение о связях между классами. Целью является минимизация таких связей (при сохранении возможности независимой модификации и повторного использования различных частей ПО);
  • для связанных классов — выбор правильного отношения, клиентского или наследования;
  • задание компонентов классов;
  • задание для классов и их компонентов правильных контрактов;
  • при задании контрактов — выбор между "требовательным" стилем (сильные предусловия, заставляющие клиентов поставлять проверенные значения исходных данных), толерантным стилем (противоположный подход) или применением промежуточного подхода, возлагая часть проверок на клиента, часть — на соответствующий метод;
  • удаление ненужных элементов;
  • недопущение дублирования кода и удаление его, если оно уже присутствует. Для этих целей применяйте наследование: если есть общие части у двух и более классов, то сделайте эти классы наследниками общего родителя, который и будет хранителем общности, присущей нескольким классам. Для устранения дублирования используйте также универсальность, кортежи и агентов;
  • использование преимуществ известных образцов проектирования;
  • проектирование инструментария, пригодного для повторного использования: простого, легкого для понимания, поставляемого с подходящими контрактами;
  • обеспечение согласованности: в программной системе подобные цели должны достигаться подобными методами. Это общее требование покрывает все вышеупомянутые требования, например, если для некоторых классов выбрано отношение наследования, то для схожей пары классов также должно выбираться наследование, а не клиентское отношение. Согласованность важна и при построении инструментария — API, если программист изучил некоторую группу классов, то он должен ожидать, что подобные соглашения будут действовать и для других классов. Эти же задачи возникают и при улучшении существующих проектов — деятельность, известная как рефакторинг. Хорошей идей является критический обзор существующей программной системы. Но помните, что лучше не делать ошибки, чем их исправлять. Проектируйте хорошо с самого начала.
  • Вне зависимости от того, идет ли речь о начальном проектировании или о рефакторинге, работа над архитектурой системы сложна и в то же время полезна. Обсуждение в этой лекции и некоторых других (лекция о топологической сортировке) дают некоторые идеи того, что нужно делать. Критерии успеха всегда одни и те же.

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

    Оценка архитектуры ПО

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

    Дальнейшее чтение

    (рис 8.4) Трюгве Реенскауг (рис 8.5) Гамма (2007)

    Trygve Reenskaug, heim.ifi.uio.no/~trygver/themes/mvc/mvc-index.html.

    Статьи по МОК.

    Трюгве Реенскауг, известный норвежский ученый в области информатики. Работая в 1979 году в Xerox PARC (известный Исследовательский центр в Пало-Альто), ввел образец МОК. Приведенная ссылка позволяет познакомиться с его работами по этой теме. Я полагаю, что оригинальная работа по МОК 1979 года все еще остается лучшей презентацией МОК.

    Erich Gamma, Richard Helm, Ralph Johnson and John Vlissides: Design Patterns, Addison-Wesley, 1994.

    Классический текст по образцам проектирования. Содержит среди многих других образцов стандартное описание образца "Наблюдатель".

    На русском языке: Эрих Гамма, Ричард Хелм, Ральф Джонсон, Джон Влиссидис: "Приемы Объектно-ориентированного проектирования. Паттерны проектирования", Питер, 2006 г.

    Bertrand Meyer: The Power of Abstraction, Reuse and Simplicity: An Object-Oriented Library for Event-Driven Design, in From Object-Orientation to Formal Methods: Essays in Memory of Ole-Johan Dahl, eds. Olaf Owe, Stein Krogdahl, Tom Lyche, Lecture Notes in Computer Science 2635, Springer-Verlag, 2004, pages 236-271.

    se.ethz.ch/~meyer/publications/lncs/events.pdf.

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

    Ключевые концепции, изученные в этой лекции

  • Проектирование, управляемое событиями, называемое также концепцией "публиковать-подписаться", приводит к системам, чье выполнение управляется откликами на события в противоположность традиционным управляющим структурам. События включаются в программной системе, часто в ответ на внешние события. Графический интерфейс — GUI-программирование — характеризует важную область приложений.
  • Ключевой абстракцией проектирования, управляемого событиями, является понятие типа события.
  • Издателями являются элементы программной системы, которые могут включать определенные типы событий. Подписчиками являются элементы программной системы, которые регистрируют действия, выполняемые при возникновении событий определенного типа. При возникновении события подписчики получают уведомление, выполняя в ответ предписанное действие.
  • В системе с одним или более обликов важным правилом проектирования является отделение обликов от ядра приложения, называемого моделью .
  • Архитектура МОК (MVC — Model — View — Controller) предполагает существование контроллера — промежуточного слоя между моделью и обликом, управляющего взаимодействием с пользователями.
  • Образец "Наблюдатель" предлагает решение проблемы за счет введения двух классов высокого уровня PUBLISHER и SUBSCRIBER, от которых наследуются издательские классы и классы подписчиков. Каждый класс подписчик задает процедуру, которая описывает действие, выполняемое в ответ на событие. Каждый издательский объект имеет закрытое от клиентов свойство, хранящее список его подписчиков. Благодаря динамическому связыванию, при включении события выполняются желаемые действия, специфические для каждого подписчика.
  • Агенты, ограниченная универсальность и кортежи позволяют дать общее решение проблемы проектирования, управляемого событиями, на основе единого повторно используемого класса, основанного на центральной абстракции:EVENT_TYPE.
  • Архитектура ПО — ключ к его качеству. Проектирование архитектуры новой системы и улучшение существующей (рефакторинг) постоянно должно быть в центре внимания, фокусируясь на простоте, надежности, расширяемости и повторном использовании.
  • Новый словарь

    Application domainПроблемная область приложенияArgument(of an event)Аргумент(события)
    Business modelБизнес-модельCatching (an event)Захват(события)
    Context (of an event)Контекст (события)Control (Windows)Элемент управления
    ControllerКонтроллерEventСобытие
    Event-drivenУправляемое событиямиEvent typeТип события
    External eventВнешнее событиеGlue codeСклеивающий код
    Handle (an event)Обработка(события)ModelМодель
    MVCМОКPublish (an event)Публикация (события)
    Publish-SubscribeПубликовать- подписатьсяRegisterРегистрация
    RefactoringРефакторингSignature (of event type)Сигнатура (типа события)
    SubscribeПодписатьсяTrigger (an event)Включение (события)
    ViewОбликWidgetВиджет, элемент управления

    Упражнения

    Словарь

    Дайте точное определение терминов словаря.

    Карта концепций

    Добавьте новые термины в карту концепций, построенную в предыдущих лекциях

    Эффективный "Наблюдатель"

    Выберите подходящее представление для списка подписчиков, адаптируя реализацию образца "Наблюдатель" так, чтобы все следующие операции выполнялись за время О(1): добавить подписчика (ничего не делать, если таковой уже существует в списке); удалить подписчика (ничего не делать, если такового нет в списке), выяснить, занесен ли потенциальный подписчик уже в список. Процедура публикации publish, если не учитывать время, затрачиваемое на реальное выполнение действия по обработке события, должна работать О(count) времени, где count — число подписчиков, реально подписанных на данный тип события. Структура данных, содержащая подписчиков, должна быть разумной и по памяти — О(count). Заметьте, что эта оптимизация также применима и к реализации классов из библиотеки Event.

    Безопасный по типам "Наблюдатель"

    Покажите, что при реализации образца "Наблюдатель" возможна схема типов, лишенная недостатков как схемы аргументов 1, так и схемы аргументов 2. Такая схема использует возможности, показанные в этой лекции и реализованные в библиотеке Event : ограниченную универсальность и кортежи. Ваше решение должно как описать изменения в классах PUBLISHER и SUBSCRIBER, так и представить наследуемые от них типичные классы издателя и подписчика.

    Страницы:

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

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

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

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

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

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

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

  • Это некоторая классика.
  • Здесь элегантно используются преимущества ОО-механизмов, таких как полиморфизм и динамическое связывание.
  • Это лучшее, что можно сделать в языках, где нет агентов, универсальности и кортежей.
  • Он дает хорошую основу для перехода к более разумному решению, изучаемому далее.
  • На следующем рисунке приведена типичная архитектура "Наблюдателя". Два общецелевых класса 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 . Динамическое связывание гарантирует, что в каждом случае будет вызываться правильный вариант. Это просто праздник лучших приемов ОО-архитектуры!

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

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

  • Как обсуждалось, возникает неприятная дилемма выбора между двумя схемами аргументов, одинаково непривлекательными: с одной стороны, опасная проверка типов аргументов в момент выполнения, чреватая ошибками, с другой — специфические, квазиидентичные классы 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
    

    Функция 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.1]в "обслуживающий" класс, к которому они все имеют доступ, например, наследуя от него.

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

    your_button.left_click.subscribe (agent p)    
    

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

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

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

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

    Реализация типа события

    Вернемся к рассмотрению сути — знакомству с реализациейEVENT_TYPE. Она подобна приводимой выше реализации PUBLISHER . Закрытый компонент subscribers хранит список подписчиков. Его сигнатура теперь такова:

    subscribers: LINKED_LIST [PROCEDURE [ANY, ARGUMENTS]]
    

    Здесь, как и прежде,LINKED_LIST — наивная структура, но вполне достаточная для нашего обсуждения (лучшую структуру можно увидеть в тексте фактического классаEVENT_TYPE из Event-библиотеки; можно также выполнить упражнение в конце лекции).

    Элементы, хранимые в списке, больше не являются подписчиками, понятие, в котором теперь архитектура решения не нуждается, — это просто агенты. Тип каждого такого агента, PROCEDURE [ANY, ARGUMENTS] указывает, что агент представляет процедуру с аргументами кортежного типа ARGUMENTS, как это определено в классе. Это значительно улучшает безопасность типов решения, в сравнении с тем, что мы видели ранее: несоответствия будут захвачены во время компиляции как плохие аргументы subscribe .

    Метод subscribe (в наивной реализации) может выглядеть так:

    subscribe (action: PROCEDURE [ANY, ARGUMENTS])
        - Зарегистрировать действие, которое должно быть выполнено для
        - событий этого типа.
      do
        subscribers.extend (action)
      ensure
        present: subscribers.has (action)
      end
    

    Использование ARGUMENTS — второго родового параметра класса в типе PROCEDURE — гарантирует, что все процедуры будут отвергнуты, если они не имеют аргументов нужного типа.

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

    publish (args: ARGUMENTS)
          — Публиковать событие подписчикам.
      do
        — Включить событие этого типа.
        from subscribers.start until subscribers.after loop
          subscribers.item.call (args)
          subscribers.forth
        end
    end
    

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

    Только что описанное решение лежит в основе библиотек Event и EiffelVision GUI. Оно широко используется для графических приложений, как небольших, так и сложных, включая само окружение EiffelStudio.

    Класс включает еще несколько деталей, которые стоит внимательно рассмотреть.

    Время чтения программ!

    Типы событий

    Познакомьтесь с текстом библиотечного классаEVENT_TYPE и убедитесь, что вы все в нем понимаете.

    Дисциплина подписчиков

    Если применять любой из приемов этой лекции — от несовершенного образца "Наблюдатель"публиковать-подписываться/codeuot; до механизма, основанного на агентах, — то следует уделить внимание проблемам производительности, которые могут приводить к потенциально разрушительным потерям памяти. Избежать их достаточно просто, если корректно определять поведение подписчиков.

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

    Не забывайте вовремя отменять подписку

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

    Чем вызвано это правило? Проблемой в использовании памяти. Из реализации subscribe, как для версии PUBLISHER в образце "Наблюдатель", так и для версии из классаEVENT_TYPE, следует, что издатель записывает в список подписчиков ссылку на объект подписчика. В приложениях GUI издатель принадлежит облику, а подписчик — модели. Поэтому объект облика сохраняет ссылку на объект модели, который, в свою очередь, может содержать другие ссылки на многие объекты модели (например, самолеты, полеты, расписание и так далее в системе управления полетами).

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

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

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

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

    handler:=agent p
    left_click.subscribe (handler)
        ... — Тогда, когда пришло время отмены подписки:
    left_click.unsubscribe ( handler)
    

    Подписка через переменную, задающую агента, вместо его непосредственного использования, как в предыдущих примерах left_click.subscribe (agent p), гарантирует, что отмена подписки применяется к тому же самому объекту (в отличие от left_click.unsubscribe (agent p), который мог быть применен к новому объекту). Если вы уже подписали данный обработчик более одного раза к данному типу события, отмена (использующая remove_all ) удалит все такие подписки.

    Архитектура ПО. Уроки

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

    Выбирайте правильные абстракции

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

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

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

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

    Вернемся к МОК

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

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

    (рис 8.2) Прямая подписка

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

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

  • Негативная сторона в том, что изменение обликов становится затруднительным. Если не ограничиваться одним обликом, то новые облики должны включать одни и те же события. Это предполагает, что различные облики не полностью различны, например, один из них может задавать интерфейс GUI, а другой — Web.
  • С другой стороны, преимущество в большой простоте. Элементы модели могут непосредственно указать, какие действия следует выполнять для специфических событий, приходящих от интерфейса. Это позволяет не иметь склеивающего кода.
  • Эта схема хороша для относительно простых программ, где интерфейс или, по меньшей мере, стиль интерфейса, известен и стабилен. Для более сложных случаев можно ввести явный контроллер, в задачу которого входит отделение от модели подписки на тип события.

    (рис 8.3) Подписка через контроллер

    Контроллер теперь становится независимой частью ПО. Его работа состоит в подписке элементов модели на типы события. Он будет иметь связи с:

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

    Модель как издатель

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

    Схему можно расширить, позволив модели играть роль издателя. Например, если элемент GUI, такой как "сектор круговой диаграммы", отражает множество значений, которое может изменяться в модели, то модель становится причиной события "change". Облики становятся подписчиками этого типа события.

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

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

    Утром инвестиции, вечером прибыль

    Общим для двух архитектур — "Наблюдатель" и "Библиотека Event" — является необходимость предварительной подписки на тип события до начала их обработки.

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

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

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

    Оценка архитектуры ПО

    Ключом качества программных систем является их архитектура, покрывающая такие аспекты, как:

  • выбор классов, основанных на подходящей абстракции данных;
  • решение о связях между классами. Целью является минимизация таких связей (при сохранении возможности независимой модификации и повторного использования различных частей ПО);
  • для связанных классов — выбор правильного отношения, клиентского или наследования;
  • задание компонентов классов;
  • задание для классов и их компонентов правильных контрактов;
  • при задании контрактов — выбор между "требовательным" стилем (сильные предусловия, заставляющие клиентов поставлять проверенные значения исходных данных), толерантным стилем (противоположный подход) или применением промежуточного подхода, возлагая часть проверок на клиента, часть — на соответствующий метод;
  • удаление ненужных элементов;
  • недопущение дублирования кода и удаление его, если оно уже присутствует. Для этих целей применяйте наследование: если есть общие части у двух и более классов, то сделайте эти классы наследниками общего родителя, который и будет хранителем общности, присущей нескольким классам. Для устранения дублирования используйте также универсальность, кортежи и агентов;
  • использование преимуществ известных образцов проектирования;
  • проектирование инструментария, пригодного для повторного использования: простого, легкого для понимания, поставляемого с подходящими контрактами;
  • обеспечение согласованности: в программной системе подобные цели должны достигаться подобными методами. Это общее требование покрывает все вышеупомянутые требования, например, если для некоторых классов выбрано отношение наследования, то для схожей пары классов также должно выбираться наследование, а не клиентское отношение. Согласованность важна и при построении инструментария — API, если программист изучил некоторую группу классов, то он должен ожидать, что подобные соглашения будут действовать и для других классов. Эти же задачи возникают и при улучшении существующих проектов — деятельность, известная как рефакторинг. Хорошей идей является критический обзор существующей программной системы. Но помните, что лучше не делать ошибки, чем их исправлять. Проектируйте хорошо с самого начала.
  • Вне зависимости от того, идет ли речь о начальном проектировании или о рефакторинге, работа над архитектурой системы сложна и в то же время полезна. Обсуждение в этой лекции и некоторых других (лекция о топологической сортировке) дают некоторые идеи того, что нужно делать. Критерии успеха всегда одни и те же.

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

    Оценка архитектуры ПО

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

    Дальнейшее чтение

    (рис 8.4) Трюгве Реенскауг (рис 8.5) Гамма (2007)

    Trygve Reenskaug, heim.ifi.uio.no/~trygver/themes/mvc/mvc-index.html.

    Статьи по МОК.

    Трюгве Реенскауг, известный норвежский ученый в области информатики. Работая в 1979 году в Xerox PARC (известный Исследовательский центр в Пало-Альто), ввел образец МОК. Приведенная ссылка позволяет познакомиться с его работами по этой теме. Я полагаю, что оригинальная работа по МОК 1979 года все еще остается лучшей презентацией МОК.

    Erich Gamma, Richard Helm, Ralph Johnson and John Vlissides: Design Patterns, Addison-Wesley, 1994.

    Классический текст по образцам проектирования. Содержит среди многих других образцов стандартное описание образца "Наблюдатель".

    На русском языке: Эрих Гамма, Ричард Хелм, Ральф Джонсон, Джон Влиссидис: "Приемы Объектно-ориентированного проектирования. Паттерны проектирования", Питер, 2006 г.

    Bertrand Meyer: The Power of Abstraction, Reuse and Simplicity: An Object-Oriented Library for Event-Driven Design, in From Object-Orientation to Formal Methods: Essays in Memory of Ole-Johan Dahl, eds. Olaf Owe, Stein Krogdahl, Tom Lyche, Lecture Notes in Computer Science 2635, Springer-Verlag, 2004, pages 236-271.

    se.ethz.ch/~meyer/publications/lncs/events.pdf.

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

    Ключевые концепции, изученные в этой лекции

  • Проектирование, управляемое событиями, называемое также концепцией "публиковать-подписаться", приводит к системам, чье выполнение управляется откликами на события в противоположность традиционным управляющим структурам. События включаются в программной системе, часто в ответ на внешние события. Графический интерфейс — GUI-программирование — характеризует важную область приложений.
  • Ключевой абстракцией проектирования, управляемого событиями, является понятие типа события.
  • Издателями являются элементы программной системы, которые могут включать определенные типы событий. Подписчиками являются элементы программной системы, которые регистрируют действия, выполняемые при возникновении событий определенного типа. При возникновении события подписчики получают уведомление, выполняя в ответ предписанное действие.
  • В системе с одним или более обликов важным правилом проектирования является отделение обликов от ядра приложения, называемого моделью .
  • Архитектура МОК (MVC — Model — View — Controller) предполагает существование контроллера — промежуточного слоя между моделью и обликом, управляющего взаимодействием с пользователями.
  • Образец "Наблюдатель" предлагает решение проблемы за счет введения двух классов высокого уровня PUBLISHER и SUBSCRIBER, от которых наследуются издательские классы и классы подписчиков. Каждый класс подписчик задает процедуру, которая описывает действие, выполняемое в ответ на событие. Каждый издательский объект имеет закрытое от клиентов свойство, хранящее список его подписчиков. Благодаря динамическому связыванию, при включении события выполняются желаемые действия, специфические для каждого подписчика.
  • Агенты, ограниченная универсальность и кортежи позволяют дать общее решение проблемы проектирования, управляемого событиями, на основе единого повторно используемого класса, основанного на центральной абстракции:EVENT_TYPE.
  • Архитектура ПО — ключ к его качеству. Проектирование архитектуры новой системы и улучшение существующей (рефакторинг) постоянно должно быть в центре внимания, фокусируясь на простоте, надежности, расширяемости и повторном использовании.
  • Новый словарь

    Application domainПроблемная область приложенияArgument(of an event)Аргумент(события)
    Business modelБизнес-модельCatching (an event)Захват(события)
    Context (of an event)Контекст (события)Control (Windows)Элемент управления
    ControllerКонтроллерEventСобытие
    Event-drivenУправляемое событиямиEvent typeТип события
    External eventВнешнее событиеGlue codeСклеивающий код
    Handle (an event)Обработка(события)ModelМодель
    MVCМОКPublish (an event)Публикация (события)
    Publish-SubscribeПубликовать- подписатьсяRegisterРегистрация
    RefactoringРефакторингSignature (of event type)Сигнатура (типа события)
    SubscribeПодписатьсяTrigger (an event)Включение (события)
    ViewОбликWidgetВиджет, элемент управления

    Упражнения

    Словарь

    Дайте точное определение терминов словаря.

    Карта концепций

    Добавьте новые термины в карту концепций, построенную в предыдущих лекциях

    Эффективный "Наблюдатель"

    Выберите подходящее представление для списка подписчиков, адаптируя реализацию образца "Наблюдатель" так, чтобы все следующие операции выполнялись за время О(1): добавить подписчика (ничего не делать, если таковой уже существует в списке); удалить подписчика (ничего не делать, если такового нет в списке), выяснить, занесен ли потенциальный подписчик уже в список. Процедура публикации publish, если не учитывать время, затрачиваемое на реальное выполнение действия по обработке события, должна работать О(count) времени, где count — число подписчиков, реально подписанных на данный тип события. Структура данных, содержащая подписчиков, должна быть разумной и по памяти — О(count). Заметьте, что эта оптимизация также применима и к реализации классов из библиотеки Event.

    Безопасный по типам "Наблюдатель"

    Покажите, что при реализации образца "Наблюдатель" возможна схема типов, лишенная недостатков как схемы аргументов 1, так и схемы аргументов 2. Такая схема использует возможности, показанные в этой лекции и реализованные в библиотеке Event : ограниченную универсальность и кортежи. Ваше решение должно как описать изменения в классах PUBLISHER и SUBSCRIBER, так и представить наследуемые от них типичные классы издателя и подписчика.

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