Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Design Patterns via C#.pdf
Скачиваний:
154
Добавлен:
17.03.2016
Размер:
13.25 Mб
Скачать

234

Мотивация

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

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

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

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

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

235

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

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

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

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

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

Результаты

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

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

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

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

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

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

236

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

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

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

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

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

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

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

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

Реализация

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

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

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

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

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

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

237

Для поддержания согласованности состояний между издателем и подписчиками используется механизм уведомлений (вызов метода 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), а эти предположения не всегда могут оказаться верны.

238

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

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

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

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

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

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

2.Реализация (протокола) плана и правил обновления состояния.

3.Обновление состояния всех подписчиков по требованию издателя.

На диаграмме ниже представлена диаграмма классов, описывающая реализацию паттерна Observer с использованием менеджера изменений ChangeManager.

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

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

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

239

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; }

240

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 следует воспринимать как вспомогательный, сугубо технический механизм-связку для уменьшения числа явных связей отношений между объектами. По сути сами связи остаются, но уже не выражаются так явно и объектно-ориентированно. Эти связи выражены с использованием функционального подхода, функциональной природы делегата и соответственно на этих связях программисты не делают акцент при анализе системы. Связи в схеме «издатель-подписчик» стали чем-то само собой разумеющимся и неявным.

Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]