Видео курс Шаблоны проектирования

Основные темы, рассматриваемые на уроке:
01:34 1 Метафора
08:38 2 Структура паттерна на языке UML (Модель вытягивания)
09:29 3 Структура паттерна на языке UML (Модель проталкивание)
09:49 4 Структура паттерна на языке C# (Модель вытягивания)
11:48 5 Структура паттерна на языке C# (Модель проталкивание)
13:56 6 Отношение паттерна к общим архитектурным моментам
18:42 7 Комбинирование издателей и подписчиков
22:19 8 Назначение паттерна

Видео урок посвящен шаблону проектирования Observer (наблюдатель), который использует связь-отношения зависимости «один ко многим» (один издатель ко многим подписчикам). При изменении состояния одного объекта (издателя), все зависящие от него объекты (подписчики) оповещаются об этом и автоматически обновляются.

Для просмотра полной версии видеокурса и получения доступа к дополнительным учебным материалам Вам необходимо оформить подписку
Оформить подписку

Паттерн Observer

Название

Наблюдатель

Также известен как

Dependents (Подчиненные), Publisher-Subscriber (Издатель-Подписчик)

Классификация

По цели: поведенческий

По применимости: к объектам

Частота использования

Высокая                -   1 2 3 4 5

Назначение

Паттерн Observer – использует связь отношения зависимости «один ко многим» (один издатель ко многим подписчикам). При изменении состояния одного объекта (издателя), все зависящие от него объекты (подписчики) оповещаются об этом и автоматически обновляются.

Введение

Паттерн Observer описывает использование важной техники ООП - «Издатель-Подписчик», другими словами, паттерн Observer описывает правильные способы организации процесса подписки на определенные события.

Кто такие издатель и подписчик в объективной реальности? Издателем может быть издательский центр - Microsoft Press который издает журнал «msdn magazine», а подписчиком может быть программист подписавшийся на данный журнал. 

После того как подписчик подписался на журнал, подписчик ожидает пока издатель издаст журнал и оповестит об этом подписчика. Имеется два способа получения подписчиком журнала. Первый способ - «метод вытягивания»: После получения уведомления от издателя о том, что журнал выпущен, подписчик должен пойти к издателю и забрать (вытянуть) журнал самостоятельно.  Второй способ – «метод проталкивания»: Издатель не уведомляет подписчика о выпуске журнала, а самостоятельно или через почту доставляет журнал подписчику и, например, бросает (проталкивает) журнал в почтовый ящик.

См. пример к главе: \019_Observer\ 004_MSDN Magazine

Структура паттерна на языке UML

Модель вытягивания (Pull model)

См. Пример к главе: \019_Observer\001_Observer [project ObserverPull]

Модель проталкивания (Push model)

См. пример к главе: \019_Observer\001_Observer [project ObserverPush]

Структура паттерна на языке C#

Модель вытягивания (Pull model)

См. пример к главе: \019_Observer\001_Observer [project ObserverPull]

Модель проталкивания (Push model)

См. пример к главе: \019_Observer\001_Observer [project ObserverPush]

Участники

  • Subject - Субъект (издатель):

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

  • Observer - Наблюдатель (подписчик):

Подписчик предоставляет интерфейс (набор методов) для обновления своего состояния при изменении состояния издателя.

  • ConcreteSubject - Конкретный субъект (конкретный издатель):

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

  • ConcreteObserver  - Конкретный наблюдатель (конкретный подписчик):

Реализует интерфейс обновления предоставляемый абстрактным классом Observer и поддерживает согласованность состояния с издателем.

Отношения между участниками

Отношения между классами

  • Абстрактный класс Subject связан связью отношения ассоциации с абстрактным классом Observer.
  • Конкретный класс ConcreteSubject связан связью отношения наследования с абстрактным классом Subject.
  • Конкретный класс ConcreteObserver связан связью отношения наследования с абстрактным классом Observer и связью отношения ассоциации с конкретным классом ConcreteSubject.

Отношения между объектами

  • ConcreteSubject (издатель) уведомляет своих наблюдателей (подписчиков) о любом изменении, которое могло бы привести к рассогласованности состояний издателя и подписчика.
  • ConcreteObserver (подписчик) после получения от ConcreteSubject (издателя) уведомления об изменении состояния, может запросить у издателя дополнительную информацию для полного согласования своего состояния.
  • Метод Notify класса ConcreteSubject (издателя) может быть вызван не только издателем самостоятельно, также этот метод могут вызывать и подписчики, и посторонние клиенты.
  • Следует отметить, что часто для сохранения унификации способа уведомления подписчиков, приходится повторно уведомлять того подписчика, который передал издателю свое новое состояние. На диаграмме взаимодействий, показан пример, когда у подписчика изменилось состояние, и этот подписчик сообщил издателю об этом. После получения от подписчика уведомления о новом состоянии, издатель начинает обновлять состояние каждого из подписчиков, которые ему известны в том числе и состояние того подписчика, который сам только что передал издателю свое новое состояние.

См. пример к главе: \019_Observer\005_Observer

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

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

Мотивация

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

Для построения программной системы чаще всего используется многослойная архитектура в которой «презентационные аспекты» (Presentation Layer) отделены от «аспектов данных» (Data Layer) и «бизнес сущностей» (Business Layer). Это значит, что классы элементов управления GUI и классы, относящиеся к бизнес логике будет располагаться в разных слоях программной системы и соответственно эти классы можно изменять независимо друг друга и работать с ними автономно.

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

Когда пользователь работает с электронными таблицами, все изменения сразу же отражаются на диаграммах, и пользователю может показаться что все объекты взаимодействуют друг с другом напрямую. Но на самом деле взаимодействие между объектами «Подписчиками/Наблюдателями» происходит через объект «Издатель/Субъект». При таком подходе, все подписчики (электронная таблица и диаграммы) зависят от издателя (Субъекта). Соответственно, если изменяется состояние одного из подписчиков, этот подписчик уведомляет о своем изменении издателя, а издатель в свою очередь уведомляет всех остальных подписчиков, тем самым приводя согласованности состояния всех подписчиков. При этом нет ограничения на количество подписчиков и для работы с одними данными (a = 50%, b = 30%, c = 20%) может существовать любое число пользовательских интерфейсов (например, диаграмм-подписчиков).

Паттерн Observer описывает способы организации отношений по принципу «издатель-подписчик». Ключевыми объектами в схеме паттерна Observer являются объект «издатель/субъект» и объект «подписчик/наблюдатель». У издателя может быть сколько угодно зависимых от него подписчиков. Все подписчики уведомляются об изменении состояния издателя и синхронизируют с издателем свое состояние. Издатель (субъект) отправляет уведомления не делая предположений об устройстве и внутренней структуре подписчиков.

Применимость паттерна

Паттерн Observer рекомендуется использовать, когда:

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

Результаты

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

Паттерн Observer обладает следующими преимуществами:

  • Абстрактная связанность издателя и подписчика.

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

В связи с тем, что издатель и его подписчики не являются сильно связанными, то они могут находиться в разных функциональных слоях системы (например, издатель в бизнес слое «Business Layer», а подписчики в слое представления «Presentation Layer»).           

Издатель, располагающийся в более низком слое «Business Layer», может уведомлять подписчиков, располагающихся в более высоком слое «Presentation Layer», не нарушая правил построения многослойной системы. Если бы издатель и подписчик представляли собой нечто единое целое (объединенная логика издателя и подписчика в одном классе), то получившийся объект либо каким-то образом пересекал бы границы функционального слоя (нарушая принципы формирования слоев и их компонентов), либо должен был бы полностью находиться в каком-то одном слое, тем самым компрометируя абстракцию определенного слоя (Например, в объективной реальности, слой «кухня», можно скомпрометировать, разместив в этом слое объект-унитаз перенесенный из слоя «туалет»).

  • Поддержка широковещательных взаимодействий.

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

Паттерн Observer обладает следующими недостатками:

  • Неожиданность обновлений.

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

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

Реализация

Полезные приемы реализации паттерна Observer:

  • Хранение ссылок в издателе на подписчиков.

С помощью этого подхода издатель может отслеживать всех подписчиков, которым требуется посылать уведомления. Однако, при наличии (очень!) большого числа издателей и всего нескольких подписчиков, такой подход может оказаться накладным, так как в каждом издателе придется создавать коллекцию ссылок на подписчиков, что в совокупности может занять большой объем памяти. Чтобы сократить объем выделяемой памяти для хранения ссылок на подписчиков, можно воспользоваться ассоциативным массивом (например, хэш-таблицей - Hashtable) в котором будут храниться соответствия «издатель-подписчик». В таком случае издатели, которые не имеют подписчиков или имеют мало подписчиков не будут расходовать память на хранение ссылок, но при этом увеличится время поиска нужного подписчика в пуле подписчиков (хэш-таблице).

  • Подписка более чем на одного издателя.

Иногда подписчик может подписаться на несколько издателей. Например, у электронной таблицы может существовать несколько источников данных. В таких случаях требуется расширить интерфейс обновления - метод Update(Subject publisher), вызываемый на подписчике, чтобы подписчик мог узнать какой издатель прислал уведомление. Издатель (Subject) может просто передать ссылку на себя в качестве аргумента метода subscriber.Update(this), тем самым сообщая подписчику (Observer) кто именно стал инициатором обновления состояния.

  • Кто может быть инициатором обновлений.

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

  1. Метод Notify вызывается непосредственно самим издателем (из методов класса Subject). Преимущество такого подхода заключается в том, что клиентам (Client) не надо помнить о необходимости вызова метода Notify класса Subject. Недостаток такого подхода в том, что при вызове определенных методов на издателе (Subject) эти методы могут вызвать метод Notify, тем самым внезапно инициировать волну обновлений подписчиков, что может стать причиной неэффективной работы программы.
  2. Метод Notify вызывается клиентом на экземпляре класса Subject. В роли клиента может выступать как объект класса (SomeClass) не входящего в структуру паттерна, так и объекты подписчики (Observer). Преимущество такого подхода заключается в том, что клиент может отложить инициирование обновления группы подписчиков до определенного времени, тем самым исключив ненужные промежуточные обновления. Недостаток такого подхода заключается в том, что у клиентов появляется дополнительная обязанность и клиент должен помнить о том, что в определенный момент нужно инициировать серию обновлений подписчиков. Это увеличит вероятность совершения ошибок клиентом, поскольку клиент должен понимать устройство подписчиков, вникать в технологические трудности работы каждого подписчика и в конце концов клиент может просто забыть вызвать метод Notify вовремя.
  • Наличие в подписчиках «висячих» ссылок на неиспользуемых издателей.

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

  • Гарантии непротиворечивости состояния издателя перед отправкой уведомления подписчикам.

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

  • Протоколы обновления: Модели вытягивания и проталкивания.

В реализациях паттерна Observer, издателю часто требуется передать подписчикам дополнительную информацию о характере изменений. Такая информация передается в качестве аргумента метода Update и объем такой передаваемой информации может время от времени изменяться.

Протокол обновления состояния подписчика имеет две модели: модель вытягивания (Pull model) и модель проталкивания (Push model)

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

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

  • Явное указание представляющих интерес изменений.

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

  • Сокрытие сложного смысла обновления.

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

  1. Организация ссылочной целостности между издателем и подписчиками, а также предоставление интерфейса (набора методов) для поддержания ссылочной целостности в актуальном состоянии. Это освобождает издателей и подписчиков хранить ссылки друг на друга.
  2. Реализация (протокола) плана и правил обновления состояния.
  3. Обновление состояния всех подписчиков по требованию издателя.
  • На диаграмме ниже представлена диаграмма классов, описывающая реализацию паттерна Observer с использованием менеджера изменений ChangeManager

См. пример к главе: \019_Observer\006_ObserverChangeManager

  • Комбинирование издателей и подписчиков.

В тех языках, которые не поддерживают множественного наследования реализации (например, C#), обычно не создаются отдельные классы Subject и Observer, а их интерфейсы комбинируются в одном классе. Такой подход позволяет создать объекты, являющиеся одновременно и издателями, и подписчиками. В языке C# имеется специальный стереотип - delegate, выражающий идею технической комбинации издателя Subject и подписчика Observer в одном объекте. В основе делегатов лежит функциональная природа несмотря на имеющееся объектно-ориентированное выражение данного стереотипа. Функциональная основа делегата, позволила практически полностью убрать использование громоздкой объектно-ориентированной подписки на события, что соответственно привело к уменьшению числа связей в программах. Предлагается рассмотреть пример использования делегатов в схеме «издатель-подписчик»:

delegate void SubjectObserver();

    class Program
    {
        // Update - логически относится к подписчику (Observer).
        static void Update()
        {
            Console.WriteLine("Hello world!");
        }

        static void Main()
        {
            SubjectObserver so = new SubjectObserver(Update);

            // Аналог вызова Notify() - логически относится к издателю (Subject).
            so.Invoke(); 
        }
    }

См. пример к главе: \019_Observer\ 002_Observer Event [001_Observer]

Приведенный пример показывает техническое, вульгарно-прямолинейное применение делегатов в схеме «издатель-подписчик», где делегат является «вещью в себе». Но, на подходе использования делегатов базируется полноценная событийная модель (event) платформы .Net. Событийная модель в .Net является логическим продолжением и более оптимальным выражением использования техники «издатель-подписчик», описываемой при помощи шаблона Observer.  Предлагается рассмотреть пример организации событийной модели с использованием конструкции языка C# - событием (event).  

// Подписчик.
    delegate void Observer(string state);    


    // Издатель.
    abstract class Subject
    {
        protected Observer observers = null;

        public event Observer Event
        {
            add { observers += value; }
            remove { observers -= value; }
        }

        public abstract string State { get; set; }
        public abstract void Notify();
    }







    // Конкретный издатель.
    class ConcreteSubject : Subject
    {
        public override string State { get; set; }

        public override void Notify()
        {
            observers.Invoke(State);
        }
    }

    class Program
    {
        static void Main()
        {
            // Издатель.
            Subject subject = new ConcreteSubject();

            // Подписчик, с сообщенным лямбда выражением.
            Observer observer = new Observer(
(observerState) => Console.WriteLine(observerState + " 1"));

            // Подписка на уведомление о событии.
            subject.Event += observer;
            subject.Event += 
(observerState) => Console.WriteLine(observerState + " 2");

            subject.State = "State ...";
            subject.Notify();

            Console.WriteLine(new string('-', 11));

            // Отписка от уведомлений.
            subject.Event -= observer;
            subject.Notify();

            // Delay.
            Console.ReadKey();
        }
    }

См. пример к главе: \019_Observer\ 002_Observer Event [002_Observer]

Нельзя смотреть «узко» на событийную модель и пытаться избавиться от объектно-ориентированного выражения издателя и подписчика в программных системах. Делегаты (delegate) и события (event) в .Net следует воспринимать как вспомогательный, сугубо технический механизм-связку для уменьшения числа явных связей отношений между объектами. По сути сами связи остаются, но уже не выражаются так явно и объектно-ориентированно. Эти связи выражены с использованием функционального подхода, функциональной природы делегата и соответственно на этих связях программисты не делают акцент при анализе системы. Связи в схеме «издатель-подписчик» стали чем-то само собой разумеющимся и неявным. 

Пример кода

Интерфейс подписчика определен в абстрактном классе Observer.

abstract class Observer
    {
        public abstract void Update(Subject theChangedSubject);
    }

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

В абстрактном классе Subject определен интерфейс издателя.

abstract class Subject
    {
        protected List<Observer> observers = new List<Observer>();

        public virtual void Attach(Observer observer)
        {
            observers.Add(observer);
            observer.Update(this);
        }

        public virtual void Detach(Observer observer)
        {
            observers.Remove(observer);
        }

        public virtual void Notify()
        {
            foreach (var o in observers)
                o.Update(this);
        }
    }

ClockTimer – это конкретный издатель, который следит за временем и оповещает подписчиков каждую секунду. Класс ClockTimer предоставляет интерфейс для получения отдельных компонентов времени: часы, минуты, секунды и т.д.

class ClockTimer : Subject
    {
        System.Threading.Timer timer;
        private TimeSpan currentTime;

        static void TimerProc(object o)
        {
            (o as ClockTimer).Tick();
        }

        public ClockTimer()
        {
            timer = new System.Threading.Timer(TimerProc, this, 1000, 1000);
        }





        public void Tick()
        {
            currentTime = DateTime.Now.TimeOfDay;
            Notify();
        }

        public int GetHour()
        {
            return currentTime.Hours;
        }

        public int GetMinute()
        {
            return currentTime.Minutes;
        }

        public int GetSecond()
        {
            return currentTime.Seconds;
        }

        public TimeSpan GetTime()
        {
            return currentTime;
        }
    }

Операция Tick вызывается через одинаковые интервалы внутренним таймером, тем самым обеспечивая правильный отсчет времени. При этом обновляется внутреннее состояние объекта ClockTimer и вызывается метод Notify для уведомления подписчиков об изменении времени.

public void Tick()
        {
            currentTime = DateTime.Now.TimeOfDay;
            Notify();
        }

Класс DigitalClock отображает время в цифровом формате.

class DigitalClock : Observer
    {
        Label digitalClockLabel;
        Subject subject;
        TimeSpan time;

        public Control GetControl
        {
            get { return digitalClockLabel; }
        }

        public DigitalClock(Control parent, Subject subject)
        {
            digitalClockLabel = new Label { Parent = parent };
            this.subject = subject;
            subject.Attach(this);
        }



        public override void Update(Subject theChangedSubject)
        {
            time = (theChangedSubject as ClockTimer).GetTime();
            digitalClockLabel.BeginInvoke(new Action(Draw));
        }

        public void Draw()
        {
            digitalClockLabel.Text = time.ToString("hh\\:mm\\:ss");
        }
    }

Класс AnalogClock отображает время в аналоговом формате.

class AnalogClock : Observer
    {
        class AnalogClockPanel : Panel
        {
            public AnalogClockPanel()
            {
                SetStyle(ControlStyles.UserPaint |
                         ControlStyles.OptimizedDoubleBuffer |
                         ControlStyles.AllPaintingInWmPaint,
                         true);
                DoubleBuffered = true;
            }
        }

        Panel analogClockPanel;
        Subject subject;
        TimeSpan time;
        Point center = new Point(50, 50);

        public AnalogClock(Control parent, Subject subject)
        {
            analogClockPanel = new AnalogClockPanel() { Parent = parent };

            analogClockPanel.Size = new System.Drawing.Size(100, 100);
            this.subject = subject;
            subject.Attach(this);
        }

        public Control GetControl
        {
            get { return analogClockPanel; }
        }
        
        public override void Update(Subject theChangedSubject)
        {
            time = (theChangedSubject as ClockTimer).GetTime();
            analogClockPanel.Invoke(new Action(Draw));
        }







        public void Draw()
        {
            analogClockPanel.Refresh();
            var g = analogClockPanel.CreateGraphics();
            var rr = analogClockPanel.ClientRectangle;
            rr.Width -= 1;
            rr.Height -= 1;
            g.DrawEllipse(new Pen(Color.Black, 1), rr);

            for (int i = 0; i < 12; i++)
            {
                var rrr = (float)(Math.PI * 2f) / 60f * (float)(i * 5);
                var from = GetDestinationPoint(rrr, 45);
                var to = GetDestinationPoint(rrr, 50);
                drawLine(g, from, to, new Pen(Color.Blue, 2));
            }

            drawW(g, (float)(Math.PI * 2f) / 12f * (float)(time.Hours % 12), 30, 
new Pen(Color.Blue, 5));
            drawW(g, (float)(Math.PI * 2f) / 60f * (float)time.Minutes, 45, 
new Pen(Color.Green, 3));
            drawW(g, (float)(Math.PI * 2f) / 60f * (float)time.Seconds, 50, 
new Pen(Color.Red, 2));
        }

        Point GetDestinationPoint(float radians, int length)
        {
            int a = center.Y - (int)(Math.Cos(radians) * (float)length);
            int b = center.X + (int)(Math.Sin(radians) * (float)length);
            return new Point(b, a);
        }

        void drawLine(Graphics g, Point from, Point to, Pen p)
        {
            g.DrawLine(p, from, to);
        }

        void drawW(Graphics g, float radians, int length, Pen p)
        {

            drawLine(g, center, GetDestinationPoint(radians, length), p);
        }
    }

Результат работы программы:

См. пример к главе: \019_Observer\007_ObserverClocks

Известные применения паттерна в .Net

Паттерн Observer, выражен в языке C# в виде идеи использования функционально-ориентированного стереотипа - делегата (delegate) и языковой конструкции - события (event), которая в свою очередь строится на использовании делегата. Так же имеется выражение паттерна Observer в FCL (Framework Class Library) виде двух интерфейсов (interface) - IObservable<out T> и IObserver<in T>. Ниже с использованием диаграмм языка DSL представлена структура паттерна Observer.

Пример кода, демонстрирует использование интерфейсов IObservable<out T> и IObserver<in T>.

Реализация подписчика:

class ConcreteObserver : IObserver<string>
    {
        string name;
        string observerState;
        IDisposable unsubscriber;

        public ConcreteObserver(string name, IObservable<string> subject)
        {
            this.name = name;
            unsubscriber = subject.Subscribe(this);
        }

        // Реализация интерфейса IObserver<T>
        public void OnCompleted()
        {
            unsubscriber.Dispose();
        }
        public void OnError(Exception error)
        {
            Console.ForegroundColor = ConsoleColor.Red;
            Console.WriteLine("Observer {0}, Error: {1}", name, error.Message);
            Console.ForegroundColor = ConsoleColor.Gray;
        }

        // Аналог Update(argument) - модель проталкивания.
        public void OnNext(string value)
        {
            observerState = value;
            Console.WriteLine("Observer {0}, State = {1}", name, observerState);
        }
    }

Реализация издателя:

class ConcreteSubject : IObservable<string>, IDisposable
    {
        public string State { get; set; }

        List<IObserver<string>> observers = new List<IObserver<string>>();

        public void Notify()
        {
            foreach (IObserver<string> observer in observers)
            {
                if (this.State == null)
                    observer.OnError(new NullReferenceException());
                else
                    observer.OnNext(this.State); // Модель проталкивания.
            }
        }

// Реализация интерфейса IObservable<T> 
// (UnSubscribe выполняется через IDisposable)

        /// <summary>
        /// Подписать подписчика.
        /// </summary>
        /// <param name="observer">Конкретный подписчик</param>
        /// <returns>Объект отписывающий подписанного подписчика</returns>
        public IDisposable Subscribe(IObserver<string> observer)
        {
            if (!observers.Contains(observer))
                observers.Add(observer);

            return new Unsubscriber(observers, observer);
        }

        // Отписать всех подписчиков.
        public void Dispose()
        {
            observers.Clear();
        }


        // Nested Class
        class Unsubscriber : IDisposable
        {
            List<IObserver<string>> observers;
            IObserver<string> observer;

            public Unsubscriber(List<IObserver<string>> observers, 
                                IObserver<string> observer)
            {
                this.observers = observers;
                this.observer = observer;
            }

            public void Dispose()
            {
                if (observers.Contains(observer))
                    observers.Remove(observer);
                else
                    observer.OnError(new Exception("Данный подписчик не подписан"));
            }
        }
    }

Использование:

class Program
    {
        static void Main()
        {
            // Создание издателя.
            ConcreteSubject subject = new ConcreteSubject();

            // Создание подписчиков.
            ConcreteObserver observer1 = new ConcreteObserver("1", subject);
            ConcreteObserver observer2 = new ConcreteObserver("2", subject);
            ConcreteObserver observer3 = new ConcreteObserver("3", subject);
            ConcreteObserver observer4 = new ConcreteObserver("4", subject);
            
            // Подписание подписчиков на издателя с получением объекта для отписки.
            IDisposable unsubscriber1 = subject.Subscribe(observer1);
            IDisposable unsubscriber2 = subject.Subscribe(observer2);
            IDisposable unsubscriber3 = subject.Subscribe(observer3);
            IDisposable unsubscriber4 = subject.Subscribe(observer4);

            using (subject)
            {
                // Попытка предоставить подписчикам некорректное состояние.
                subject.State = null;
                subject.Notify();
                Console.WriteLine(new string('-', 70) + "1");

                // Отписка первого подписчика через 
                // ConcreteSubject.Unsubscriber.Dispose()
                using (unsubscriber1)
                {
                    // Попытка предоставить подписчикам корректное состояние.
                    subject.State = "State 1 ...";
                    subject.Notify();
                }

                Console.WriteLine(new string('-', 70) + "2");

                // State 2 - получат только три подписчика 
                // которые остались подписанными.
                subject.State = "State 2 ...";
                subject.Notify();

                Console.WriteLine(new string('-', 70) + "3");

                // Отписка второго подписчика через ConcreteObserver.OnCompleted()
                observer2.OnCompleted();
                
                // State 3 - получат только 2 подписчика 
                // которые остались подписанными.
                subject.State = "State 3 ...";
                subject.Notify();
            } // observers.Clear()

            Console.WriteLine(new string('-', 70) + "4");

            // Попытка отписать уже отписанного подписчика, обрабатывается в
            // ConcreteSubject.Unsubscriber.Dispose()
            observer4.OnCompleted();

            // Delay.
            Console.ReadKey();
        }
    }

Результат работы программы:

См. пример к главе: \019_Observer\003_IObserver

© 2017 ITVDN, все права защищены