Паттерн "Заместитель"

advertisement
Паттерн Заместитель
(Proxy)
Определение
Определение
Заместитель — структурный паттерн,
замещающий исходный объект и
контролирующий его работу, перехватывая
его вызовы.
Мотивация

Обращение к удаленным объектам.
Удаленный заместитель предоставляет локального представителя
целевого объекта, находящегося в другом адресном пространстве.

Ускорение загрузки сущностей, содержащих ресурсоемкие
объекты.
Виртуальный заместитель создает «тяжелые» или недоступные в
данный момент объекты по требованию.


Контроль доступа к исходному объекту.
Защищающий заместитель контролирует доступ к исходному
объекту. Такие заместители полезны, когда для разных объектов
определены различные права доступа.
Выполнение дополнительных действий при доступе к объекту.



подсчет числа ссылок на реальный объект;
загрузка объекта в память при первом обращении к нему;
проверка и установка блокировки на реальный объект при обращении к
нему.
Диаграмма классов
Участники

Proxy — заместитель.
Хранит ссылку, которая позволяет заместителю
обратиться к реальному субъекту. Предоставляет
интерфейс, идентичный интерфейсу Subject, так что
заместитель всегда может быть подставлен вместо
реального субъекта. Контролирует доступ к реальному
субъекту и может отвечать за его создание и удаление.

Subject — субъект.
Определяет общий для RealSubject и Proxy интерфейс,
так что класс Proxy можно использовать везде, где
ожидается RealSubject.

RealSubject — реальный субъект.
Определяет реальный объект, представленный
заместителем.
Плюсы



Инкапсуляция факта нахождения объекта
в другом адресном пространстве.
Инкапсуляция алгоритмов оптимизации
(например, создание объекта по
требованию).
Инкапсуляция алгоритмов разрешения
прав доступа.
Пример №1
Рассмотрим пример виртуального заместителя.
Пусть есть класс, представляющий некоторый графический
элемент:
public abstract class Graphic
{
public abstract Size GetSize();
public abstract void Draw();
}
Пример №1
Определим также класс растрового изображения,
который будет являться наследником класса Graphic:
public class BitmapImage : Graphic {
public BitmapImage(Stream content) {
LoadImage(content);
}
private void LoadImage(Stream content) {
// реализация пропущена
}
public override Size GetSize() {
// реализация пропущена
}
public override void Draw() {
// реализация пропущена
}
}
Пример №1
Обратим внимание, что при создании экземпляра
класса BitmapImage выполняется ресурсоѐмкая
операция загрузки изображения.
Создадим класс заместителя, который будет загружать
изображение из файла в момент, когда требуется его
отрисовать.
Размер изображения чаще всего можно вычислить, не
загружая всего файла, поэтому при создании
экземпляра класса заместителя загрузим размер
изображения из файла в поле.
Пример №1
public class BitmapImageProxy : Graphic {
private string fileName;
private Size size;
private BitmapImage image;
public BitmapImageProxy(string fileName) {
this.fileName = fileName;
this.size = GetImageSize(fileName);
}
public override Size GetSize() {
if (image == null)
return size;
return image.GetSize();
}
public override void Draw() {
if (image == null)
image = new BitmapImage(new FileStream(fileName, FileMode.Open));
image.Draw();
}
}
Пример №2
Рассмотрим фрагмент приложения по учѐту кадров.
Пусть сущность работника определена следующим
образом:
public class Employee
{
public int Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
}
Пример №2
Определим также интерфейс, который будет
предоставлять методы для доступа к имеющимся
данным о сотрудниках:
public interface IEmployeeRegistry
{
IEnumerable<Employee> GetEmployees();
Employee FindEmployee(int id);
// ...
}
Пример №2
Реализация данного интерфейса, которая получает значения,
подключаясь к базе данных, может быть следующей:
public class EmployeeRegistry : IEmployeeRegistry {
private readonly SqlConnection connection;
public EmployeeDataSource(SqlConnection connection) {
this.connection = connection;
}
public IEnumerable<Employee> GetEmployees() {
using (SqlCommand command = connection.CreateCommand()) {
command.CommandText =
"SELECT ID, FisrtName LastName FROM Employee";
using (IDataReader reader = command.ExecuteReader()) {
// ...
}
}
}
}
Пример №2
Предположим, что доступ к базе данных
закрыт для машины, на которой работает
клиентское приложение, и мы определили
сервер приложений.
В таком случае уместно использование
удалѐнного заместителя, который
подключается к серверу приложений по
известному протоколу и получает все
необходимые данные.
Пример №2
public class RemoteEmployeeRegistry : IEmployeeRegistry
{
private readonly string host;
private readonly int port;
public RemoteEmployeeRegistry (string host, int port)
{
this.host = host;
this.port = port;
}
public IEnumerable<Employee> GetEmployees()
{
Socket connection = new Socket(AddressFamily.InterNetwork,
SocketType.Stream, ProtocolType.IP);
connection.Connect(host, port);
connection.Send( ... );
connection.Receive( ... );
}
}
Пример №2
Пример №2
Для реализации подобной подобной функциональности
используются специальные библиотеки:
Для платформы .NET:
 Windows Communication Foundation (WCF)
http://msdn.microsoft.com/en-us/netframework/aa663324
 .NET Remoting
http://msdn.microsoft.com/en-us/library/ms973857.aspx
Для платформы Java:
 Remote Method Invocation
http://www.oracle.com/technetwork/java/javase/tech/indexjsp-136424.html
Пример №3
Другим примером заместителя является обѐртка, которая
ограничивает доступ к данным или функциям объекта.
Предположим, что в рассмотренном выше приложении требуется
ограничить доступ к информации о сотрудниках. Пусть требуется
разрешить одним сотрудникам получать информацию о других,
только в том случае, если они являются их прямыми
подчинѐнными.
Определим в сущности сотрудника свойство Manager, которое
указывает на его прямого руководителя:
public class Employee {
public int Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public Employee Manager { get; set; }
}
Пример №3
Класс заместителя может принять в конструктор
информацию о сотруднике, который в данный момент
работает с системой, и интерфейс, который получает
информацию о сотрудниках:
public class EmployeeProtectedRegistry : IEmployeeRegistry
{
private readonly Employee account;
private readonly IEmployeeRegistry registry;
public EmployeeDataSourceProxy(Employee account,
IEmployeeRegistry registry)
{
this.account = account;
this.registry = registry;
}
}
Пример №3
Реализация методов в классе заместителя может быть
следующей:
public class EmployeeProtectedRegistry : IEmployeeRegistry
{
public IEnumerable<Employee> GetEmployees()
{
foreach(Employee employee in registry.GetEmployees())
if ((employee.Manager == account) || (employee == account))
yield return employee;
}
public Employee FindEmployee(int id)
{
Employee result = registry.FindEmployee(id);
if (result.Manager == account || result == account)
return result;
return null;
}
}
Пример №3
Пусть в базе имеются данные о следующих
сотрудниках:
Id
1
2
3
4
5
6
7
8
FullName
Остап Бендер
Шура Балаганов
Михаил Паниковский
Адам Козлевич
Иван Полыхаев
Егор Скумбриевич
Александр Корейко
Никита Пряхин
Manager_id
NULL
1
1
1
NULL
5
5
NULL
Пример №3
Обратимся к следующему фрагменту кода:
SqlConnection connection = new SqlConnection( ... );
Employee currentEmployee; // Остап Бендер
IEmployeeRegistry employeeRegistry = new EmployeeProtectedRegistry(
currentEmployee,
new EmployeeRegistry(connection));
foreach (Employee employee in employeeRegistry.GetEmployees())
Console.WriteLine(employee.FirstName + " " + employee.LastName);
Если текущим сотрудником является Остап Бендер, то данный код
напечатает список его прямых подчинѐнных и его самого:
Остап Бендер
Шура Балаганов
Михаил Паниковский
Адам Козлевич
Паттерн Наблюдатель
(Observer)
Определение и мотивация
Определение
Наблюдатель — паттерн поведения объектов, определяющий
зависимость типа «один ко многим» между объектами таким образом,
что при изменении состояния одного объекта все зависящие от него
оповещаются об этом событии.
Мотивация

У некоторой абстракции есть два аспекта, один из которых зависит
от другого
Инкапсуляции этих аспектов в разные объекты позволяют
изменять и повторно использовать их независимо.

Когда при модификации одного объекта требуется изменить и
другие, но неизвестно, сколько именно объектов нужно изменить.

Когда один объект должен оповещать других, не делая
предположений, не владея информацией об уведомляемых
объектах. Другими словами, вы не хотите, чтобы объекты были
тесно связаны между собой.
Плюсы


Абстрактная связанность субъекта и наблюдателя
Субъект имеет информацию лишь о том, что у него есть ряд
наблюдателей, каждый из которых подчиняется простому
интерфейсу абстрактного класса Observer. Субъекту неизвестны
конкретные классы наблюдателей. Таким образом, связи между
субъектами и наблюдателями носят абстрактный характер и
сведены к минимуму. Поскольку субъект и наблюдатель не
являются тесно связанными, то они могут находиться на разных
уровнях абстракции системы.
Поддержка широковещательных коммуникаций
В отличие от обычного запроса для уведомления, посылаемого
субъектом, не нужно задавать определенного получателя.
Уведомление автоматически поступает всем подписавшимся на
него объектам. Субъекту не нужна информация о количестве таких
объектов, от него требуется всего лишь уведомить своих
наблюдателей. Поэтому мы можем в любое время добавлять и
удалять наблюдателей. Наблюдатель сам решает, обработать
полученное уведомление или игнорировать его.
Диаграмма классов
Участники

Subject — субъект
Регистрирует своих наблюдателей. За субъектом может «следить»
любое число наблюдателей. Предоставляет интерфейс для
регистрации и, соответственно, отписки наблюдателей.

Observer — наблюдатель
Определяет интерфейс для уведомления подписчиков, т.е.
объектов, заинтересованных в изменениях субъекта.

ConcreteSubject — конкретный субъект
Сохраняет состояние, представляющее интерес для любого
конкретного наблюдателя ConcreteObserver. Посылает
информацию своим наблюдателям, когда происходит изменение.

ConcreteObserver — конкретный наблюдатель
Хранит ссылку на объект класса ConcreteSubject (для того чтобы
потом обращаться к нему за синхронизацией данных). Сохраняет
данные, которые должны быть согласованы с данными субъекта.
Реализует интерфейс обновления, определенный в классе
Observer, чтобы «быть уведомленным» об изменениях
ConcreteSubject-а.
Пример №1
Рассмотрим задачу отображения часов. Предположим, что нам
необходимо отобразить часы двух типов: цифровые и стрелочные.
Разумеется, все часы должны показывать одинаковое время.
Определим класс Subject:
public class Subject {
List<IObserver> observers = new List<IObserver>();
public void Attach(IObserver observer) {
observers.Add(observer);
}
public void Detach(IObserver observer) {
observers.Remove(observer);
}
public void Notify() {
foreach(IObserver observer in observers)
observer.Update(this);
}
}
Пример №1
Наследником класса Subject определим класс ClockTimer, который
оповещает подписчиков об изменении текущего времени и
предоставляет интерфейс для его получения:
public class ClockTimer : Subject {
private Timer timer;
public ClockTimer() {
timer = new Timer();
timer.Interval = 1000;
timer.Tick += new EventHandler(TimerTickHandler);
timer.Enabled = true;
}
private void TimerTickHandler(object sender, EventArgs e) {
Notify();
}
public DateTime CurrentDateTime {
get { return DateTime.Now; }
}
}
Пример №1
Определим интерфейс Observer, который должны
реализовать объекты, желающие получать оповещения:
public interface IObserver
{
void Update(Subject sender);
}
Обратим внимание на сигнатуру метода Update: она не
содержит никакой информации о характере изменений,
и объект, получивший уведомление, должен запросить
всю необходимую информацию у объекта-отправителя.
Такая модель получила название модель вытягивания
(pull model).
Пример №1
Класс, представляющий цифровые часы, выглядит
следующим образом:
public class DigitalClock : IObserver {
private ClockTimer timer;
public DigitalClock(ClockTimer timer) {
this.timer = timer;
timer.Attach(this);
}
public void Draw() {
Console.WriteLine("Digital clock time: {0}:{1}:{2}",
timer.CurrentDateTime.Hour, timer.CurrentDateTime.Minute,
timer.CurrentDateTime.Second
);
}
public void Update(Subject sender) {
if (timer == sender)
Draw();
}
}
Пример №1
Класс, представляющий стрелочные часы, выглядит
следующим образом:
public class AnalogClock : IObserver {
private ClockTimer timer;
private Canvas canvas;
public AnalogClock(ClockTimer timer, Canvas canvas) {
this.canvas = canvas;
this.timer = timer;
timer.Attach(this);
}
public void Draw() {
// Рисование стрелочных часов
}
public void Update(Subject sender) {
if (timer == sender)
Draw();
}
}
Пример №1
Полученные классы могут быть использованы таким
образом:
public static void ShowClocks()
{
ClockTimer timer = new ClockTimer();
DigitalClock clock1 = new DigitalClock(timer);
AnalogClock clock2 = new DigitalClock(timer, GetTargetCanvas());
}
Пример №2
Рассмотрим реализацию паттерна наблюдатель
посредством механизма событий языка C#.
В качестве примера рассмотрим следующую задачу:
реализовать класс, который создаѐт заданную
директорию, если она ещѐ не существует, при этом
 перед проверкой на существование необходимо
вывести строку «Поиск…»,
 перед созданием директории вывести строку
«Создание…»,
 после успешного завершения операции вывести
«[OK]».
Пример №2
Определим в классе, отвечающем за создание
директорий, события, которые будут инициироваться
перед поиском и созданием директории, а также после
еѐ успешного создания:
public class DirectoryCreator
{
public event EventHandler<DirectoryEventArgs> SearchingDirectory;
public event EventHandler<DirectoryEventArgs> BeforeCreatingDirectory;
public event EventHandler<DirectoryEventArgs> DirectoryCreated;
}
Пример №2
Дополнительным параметром события передают
экземпляр класса DirectoryEventArgs:
public class DirectoryEventArgs : EventArgs {
public DirectoryEventArgs(string directory) {
this.Directory = directory;
}
public string Directory
{ get; private set; }
}
Обратим внимание на то, что подписчики всю
необходимую информацию получают, через параметры
события и не обязаны делать запросы объектуотправителю. Такая модель получила название
модели проталкивания (push model).
Пример №2
Метод Create в классе DirectoryCreator будет выглядеть
следующим образом:
public class DirectoryCreator {
public void Create(string directoryName) {
if (SearchingDirectory != null)
SearchingDirectory(this, new DirectoryEventArgs(directoryName));
if (!Directory.Exists(directoryName))
{
if (BeforeCreatingDirectory != null)
BeforeCreatingDirectory(this,
new DirectoryEventArgs(directoryName));
Directory.CreateDirectory(directoryName);
if (DirectoryCreated != null)
DirectoryCreated(this,
new DirectoryEventArgs(directoryName));
}
}
}
Пример №2
Заметим, что в вышеприведенном примере дублируется
проверка на null. Методы расширения помогают
сократить дублированием путѐм определения
следующего метода:
public static class EventHandlerUtils {
public static void Fire<T>(this EventHandler<T> eventHandler,
object sender, T eventArgs)
where T : EventArgs
{
if (eventHandler != null)
eventHandler(sender, eventArgs);
}
public static void Fire(this EventHandler eventHandler,
object sender, EventArgs eventArgs)
{
if (eventHandler != null)
eventHandler(sender, eventArgs);
}
}
Пример №2
Метод Create из нашего примера преобразуется
следующим образом:
public class DirectoryCreator {
public void Create(string directoryName)
{
SearchingDirectory.Fire(this,
new DirectoryEventArgs(directoryName));
if (!Directory.Exists(directoryName))
{
BeforeCreatingDirectory.Fire(this,
new DirectoryEventArgs(directoryName));
Directory.CreateDirectory(directoryName);
DirectoryCreated.Fire(this,
new DirectoryEventArgs(directoryName));
}
}
}
Пример №2
Кроме того, удобно выделить инициирование событий в
специальные методы:
public class DirectoryCreator
{
protected virtual void DoSearchingDirectory(string directoryName)
{
SearchingDirectory.Fire(this, new DirectoryEventArgs(directoryName));
}
public void Create(string directoryName)
{
DoSearchingDirectory(directoryName);
if (!Directory.Exists(directoryName))
{
DoBeforeCreatingDirectory(directoryName);
Directory.CreateDirectory(directoryName);
DoDirectoryCreated(directoryName);
}
}
}
Пример №3
Рассмотрим пример использования событий в языке
С++. Воспользуемся реализацией событий Signals2 из
библиотеки Boost.
Рассмотрим вводный пример:
void print_hello()
{
std::cout << "Hello, World!!!" << std::endl;
}
// Определяем событие и сигнатуру его обработчиков
boost::signals2::signal<void ()> simple_event;
// Подключаем обработчик
simple_event.connect(&print_hello);
// Инициируем событие
simple_event();
Пример №3
Отключение событий производится посредством
вызова метода disconnect у экземпляра класса
boost::signals2::connection, который возвращается
методом connect при подключении обработчика:
boost::signals2::signal<void ()> simple_event;
boost::signals2::connection eventConnection =
simple_event.connect(&print_hello);
simple_event();
eventConnection.disconnect();
Пример №3
Рассмотрим модель объектов базы данных. Среди типов объектов
базы данных присутствует синоним, который содержит ссылку на
объект. Необходимо обеспечить корректное функционирование
данного класса, даже если объект, на который он ссылался, был
удалѐн.
Рассмотрим пример клиентского кода:
DBObject * table = new DBObject("SomeTableName");
DBObjectSynonym * firstSynonym = new DBObjectSynonym("FirstSynonym", table);
DBObjectSynonym * secondSynonym =new DBObjectSynonym("SecondSynonym",table);
std::cout << firstSynonym->GetLinkedObjectName() << std::endl;
std::cout << secondSynonym->GetLinkedObjectName() << std::endl;
delete table;
std::cout << firstSynonym->GetLinkedObjectName() << std::endl;
std::cout << secondSynonym->GetLinkedObjectName() << std::endl;
delete firstSynonym;
delete secondSynonym;
Пример №3
Для реализации подобной функциональности объект должен
сообщить о том, что он будет уничтожен. Используем для
уведомления объекты из библиотеки Signals2:
class DBObject {
private:
typedef signal<void ()> OnDestroySignal;
OnDestroySignal onDestroy;
string name;
public:
DBObject(const string & name): name(name) { }
virtual ~DBObject() { onDestroy(); }
string GetName() const { return name; }
boost::signals2::connection
AttachDestroyHandler(const OnDestroySignal::slot_type & handler)
{
return onDestroy.connect(handler);
}
};
Пример №3
Класс DBObjectSynonym должен установить обработчик
события OnDestroy, в котором обновить значение
ссылки:
class DBObjectSynonym : public DBObject {
private:
boost::signals2::connection linkedObjectConnection;
DBObject * linkedObject;
void LinkedObjectDestoryHandler() {
linkedObject = NULL;
}
public:
DBObjectSynonym(const string & name, DBObject * object) :
DBObject(name), linkedObject(object)
{
linkedObjectConnection = linkedObject->AttachDestroyHandler(
boost::bind(&DBObjectSynonym::LinkedObjectDestoryHandler, this)
);
}
};
Пример №3
В деструкторе DBObjectSynonym необходимо отключить
обработчик события, а метод GetLinkedObjectName должен
учитывать тот факт, что ссылка может нулевой:
class DBObjectSynonym : public DBObject {
private:
boost::signals2::connection linkedObjectConnection;
DBObject * linkedObject;
public:
~DBObjectSynonym()
{
linkedObjectConnection.disconnect();
}
string GetLinkedObjectName() const
{
if (linkedObject)
return linkedObject->GetName();
else
return "No object assigned";
}
};
Паттерн
Посетитель (Visitor)
Определение и мотивация
Определение
Посетитель — паттерн поведения объектов, описывающий операцию,
выполняемую с каждым объектом из некоторой структуры и позволяющий
определять операцию, не изменяя этих классов.
Мотивация



В структуре присутствуют объекты многих классов с различными
интерфейсами, и вы хотите выполнять над ними операции, зависящие от
конкретных классов.
Над объектами, входящими в состав структуры, надо выполнять
разнообразные, не связанные между собой операции, и вы не хотите
«засорять» классы такими операциями.
Посетитель позволяет объединить родственные операции, поместив их в
один класс. Если структура объектов является общей для нескольких
приложений, то паттерн посетитель позволит в каждое приложение
включить только относящиеся к нему операции.
Классы, устанавливающие структуру объектов, изменяются редко, но
новые операции над этой структурой добавляются часто.
При изменении классов, представленных в структуре, нужно будет
переопределить интерфейсы всех посетителей, а это может вызвать
затруднения. Поэтому если классы меняются достаточно часто, то,
вероятно, лучше определить операции прямо в них.
Плюсы

Упрощает добавление новых операций
С помощью посетителей легко добавлять операции, зависящие от
компонентов сложных объектов. Для определения новой операции над
структурой объектов достаточно просто ввести нового посетителя.

Объединяет родственные операции и отсекает те, которые не имеют к
ним отношения
Родственное поведение не разносится по всем классам, присутствующим в
структуре объектов, оно локализовано в посетителе. Не связанные друг с
другом функции распределяются по отдельным подклассам класса Visitor.
Все относящиеся к алгоритму структуры данных можно скрыть в
посетителе.

Посещение различных иерархий классов
Посетителю разрешено посещать объекты, не имеющие общего
родительского класса. В интерфейс класса Visitor можно добавить
операции для объектов любого типа.

Аккумулирование состояния
Посетители могут аккумулировать информацию о состоянии при
посещении объектов структуры. Если не использовать этот паттерн,
состояние придется передавать в виде дополнительных аргументов
операций, выполняющих обход, или хранить в глобальных переменных.
Диаграмма классов
Участники

Visitor — посетитель
Объявляет операцию VisitXXX для каждого класса ConcreteElement в
структуре объектов. Имя и сигнатура этой операции идентифицируют
класс, который посылает посетителю запрос Visit-а. Имя метода позволяет
посетителю определить, элемент какого конкретного класса он посещает, а
ссылка на этот элемент, передающаяся при вызове, — обращаться к нему
через его интерфейс.

ConcreteVisitor — конкретный посетитель
Реализует все операции, объявленные в классе Visitor. Каждая операция
реализует фрагмент алгоритма, определенного только для класса
соответствующего объекта в структуре. Класс ConcreteVisitor
предоставляет контекст для этого алгоритма и сохраняет его локальное
состояние. Часто в этом состоянии аккумулируются результаты,
полученные в процессе обхода структуры.


Element — элемент
Определяет операцию Accept, которая принимает посетителя в качестве
аргумента.
ConcreteElement — конкретный элемент
Реализует операцию Accept, принимающую посетителя как аргумент.
Реализует он ее каждый раз по-разному, а именно, вызывает только тот
Visit-метод, который для него предназначен.
Пример №1
Пусть некоторая предметная область содержит
сущности точки и линии. Необходимо обеспечить вывод
информации об этих объектах на консоль и их
рисование на канве.
Сущность точки определена так:
public class Point {
public Point(int top, int left) {
this.Top = top;
this.Left = left;
}
public
{ get;
public
{ get;
}
int Top
set; }
int Left
set; }
Пример №1
Сущность линии определяется таким образом:
public class Line {
Point start = new Point();
Point end = new Point();
public Line(Point start, Point end) {
this.start.Left = start.Left;
this.start.Top = start.Top;
this.end.Left = end.Left;
this.end.Top = end.Top;
}
Point Start {
get { return start; }
}
Point End {
get { return end; }
}
}
Пример №1
Определим интерфейс посетителя для объектов
обозначенной выше предметной области:
public interface IVisitor
{
void VisitPoint(Point point);
void VisitLine(Line line);
}
Пример №1
Создадим первую реализацию посетителя — вывод в
консоль информации об объектах:
public class ConsoleDrawer : IVisitor
{
public void VisitPoint(Point point)
{
Console.WriteLine("Точка ({0}, {1})",
point.Top, point.Left);
}
public void VisitLine(Line line)
{
Console.WriteLine("Линия ({0}, {1}) - ({2}, {3})",
line.Start.Top, line.Start.Left,
line.End.Top, line.End.Left);
}
}
Пример №1
Другая реализации интерфейса предназначена для
рисования объектов на канве.
public class CanvasDrawer : IVisitor
{
Canvas canvas;
public CanvasDrawer(Canvas canvas)
{
this.canvas = canvas;
}
public void VisitPoint(Point point)
{
canvas.PutPoint(point.Left, point.Top);
}
public void VisitLine(Line line)
{
canvas.DrawLine(line.Start.Left, line.Start.Top,
line.End.Left, line.End.Top);
}
}
Пример №1
Объявляем методы Accept для объектов линии и точки:
public class Point
{
public virtual void Accept(IVisitor visitor)
{
visitor.VisitPoint(this);
}
}
public class Line
{
public virtual void Accept(IVisitor visitor)
{
visitor.VisitLine(this);
}
}
Пример №1
Пример использования операции вывода в консоль
может быть таким:
public static void UseConsoleDrawer()
{
Point point1 = new Point(2, 1);
Line line1 = new Line(
new Point(1, 1),
new Point(2, 3)
);
ConsoleDrawer drawer = new ConsoleDrawer();
point1.Accept(drawer);
line1.Accept(drawer);
}
Пример №1
Операции, которые определяет посетитель, могут быть
самыми разными, например, это может быть операция
изменения положения:
public class ChangePositionVisitor : IVisitor
{
private int topDiff;
private int leftDiff;
public ChangePositionVisitor(int topDiff, int leftDiff)
{
this.topDiff = topDiff;
this.leftDiff = leftDiff;
}
}
Пример №1
public class ChangePositionVisitor : IVisitor
{
public void VisitPoint(Point point)
{
point.Left += leftDiff;
point.Top += topDiff;
}
public void VisitLine(Line line)
{
line.Start.Left += leftDiff;
line.Start.Top += topDiff;
line.End.Left += leftDiff;
line.End.Top += topDiff;
}
}
Пример №2
Рассмотрим схематичный пример применения паттерна
посетитель для составного объекта.
Пусть есть некий базовый класс элемента, у которого
определѐн метод Accept:
public class Element
{
public virtual void Accept(IVisitor visitor)
{
visitor.VisitElement(this);
}
}
Пример №2
Наследник базового класса элемента — составной
объект, может определять метод Accept следующим
образом:
public class CompositeElement : Element
{
private List<Element> children = new List<Element>;
// Реализация класса CompositeElement ...
public override void Accept(IVisitor visitor)
{
foreach(Element element in children)
element.Accept(visitor);
visitor.VisitCompositeElement(this);
}
}
Пример №3
Рассмотрим особенности реализации паттерна Посетитель
на языке C++ на примере обхода элементов RichTextдокумента. Элементы документа могут быть либо
параграфом, либо растровым изображением.
Техника реализации данного паттерна на языке C++ состоит
из следующих ключевых моментов:
 Создание абстрактного шаблонного класса Visitor<T>,
который описывает посетителя для одного класса.
 Создание класса конкретного посетителя с
использованием множественного наследования от
классов Visitor с указанием типа объекта, который может
быть проинспектирован.
 Создание класса BaseVisitable для упрощения создания
инспектируемых классов.
Пример №3
Определим классы BaseVisitor и Visitor<T>.
Класс BaseVisitor — фиктивный класс, составляющий
основу иерархии инспектирующих классов:
class BaseVisitor
{
public:
virtual ~BaseVisitor() { }
};
Visitor<T> определяет интерфейс для инспектирования
какого-либо конкретного типа:
template<class T>
class Visitor
{
public:
virtual void Visit(T&) = 0;
};
Пример №3
Какой-либо конкретный посетитель определяется
следующим образом:
class SomeVisitor :
public BaseVisitor,
public Visitor<RasterBitmap>, // для инспектирования RasterBitmap
// для инспектирования Paragraph
public Visitor<Paragraph>
{
public:
void Visit(RasterBitmap&);
void Visit(Paragraph&);
};
Пример №3
Создадим класс BaseVisitable, определив в нем
абстрактную функцию Accept:
class BaseVisitable
{
public:
virtual ~BaseVisitable() { }
virtual void Accept(BaseVisitor&) = 0;
};
Пример №3
Теперь необходимо в каждом наследнике этого класса
перекрыть функцию Accept, написав в каждой
реализации такой код:
class Paragraph : public DocElement
{
public:
void Accept(BaseVisitor & visitor)
{
if (Visitor<Paragraph>* p =
dynamic_cast<Visitor<Paragraph>*>(&visitor))
p->Visit(*this);
}
};
Пример №3
Для сокращения дублирования определим в классе
BaseVisitable шаблонную функцию следующего вида:
class BaseVisitable
{
protected:
template<class T>
void AcceptImpl(T& visited, BaseVisitor& visitor)
{
if (Visitor<T>* p = dynamic_cast<Visitor<T>*>(&visitor))
p->Visit(visited);
}
};
Пример №3
Чтобы избежать повторного написания одного и того же
кода при создании инспектируемых, классов создадим
макрос:
#define DEFINE_VISITABLE() \
virtual void Accept(BaseVisitor& visitor)
{ AcceptImpl(*this, visitor); }
\
Пример №3
Теперь инспектируемые элементы создаются
следующим образом:
class DocElement : public BaseVisitable
{
public:
DEFINE_VISITABLE()
};
class Paragraph : public DocElement
{
public:
DEFINE_VISITABLE()
};
class RasterBitmap : public DocElement
{
public:
DEFINE_VISITABLE()
};
Пример №3
Операция для сбора статистики по документу будет
иметь такой вид:
class DocumentStatistics : public BaseVisitor,
public Visitor<Paragraph>,
public Visitor<RasterBitmap>
{
private:
int chars;
int words;
int images;
public:
void Visit(RasterBitmap& bitmap) {
images++;
}
void Visit(Paragraph& paragraph) {
chars += paragraph.GetCharCount();
words += paragraph.GetWordCount();
}
};
Паттерн Хранитель
(Memento)
Определение и мотивация
Определение
Хранитель — паттерн поведения объектов,
позволяющий, не нарушая инкапсуляцию,
сохранять и восстанавливать состояние объекта.
Мотивация


Необходимо сохранить мгновенный снимок
состояния объекта (или его части), чтобы
впоследствии объект можно было восстановить
в том же состоянии.
Прямое получение указанного выше состояния
раскрывает детали реализации и нарушает
инкапсуляцию объекта.
Предпосылки использования
Рассмотрим графический редактор, который
поддерживает связанность объектов.
В качестве основного элемента рассмотрим некоторую
прямоугольную область:
public class Rectangle
{
public Rectangle(Point leftTop, Size size)
{
LeftTop = leftTop;
Size = size;
}
public Point LeftTop { get; set; }
public Size Size { get; set; }
}
Предпосылки использования
Связь между объектами будет обеспечивать
следующий простой класс:
public class Relation
{
public Rectangle FirstRectangle { get; private set; }
public Rectangle SecondRectangle { get; private set; }
public Relation(Rectangle firstRectangle,
Rectangle secondRectangle)
{
FirstRectangle = firstRectangle;
SecondRectangle = secondRectangle;
}
}
Предпосылки использования
Для того чтобы обозначить связь между объектами,
будет использоваться следующий интерфейс:
public class Rectangle
{
private readonly List<Relation> relations = new List<Relation>();
public IEnumerable<Relation> Relations
{
get { return relations; }
}
public Relation AddRelation(Rectangle target)
{
Relation result = new Relation(this, target);
relations.Add(result);
return result;
}
}
Предпосылки использования
При перемещении фигур редактор должен сам
обновлять положение связей. Для обеспечения такой
функциональности воспользуемся классом
RelationSolver, который будет «знать» про все связи и
вычислять их положение:
public class RelationSolver
{
private readonly List<Relation> relations =
new List<Relation>();
public void AddRelation(Relation relation)
{
relations.Add(relation);
}
}
Предпосылки использования
В самом простом случае данный объект может иметь
примерно следующую реализацию:
public class RelationSolver
{
private readonly Dictionary<Relation, List<Point>> paths
= new Dictionary<Relation, List<Point>>();
public IEnumerable<Point>
GetRelationGraphicsPath(Relation relation)
{
return paths[relation];
}
public void SolveRelationPositions()
{
// ...
}
}
Предпосылки использования
Рассмотрим теперь ситуацию, когда нам требуется
выполнить откат операции перемещения фигуры с
учѐтом положения линий.
Пусть две фигуры находились в следующем
положении:
Предпосылки использования
Затем нижнюю фигуру переместили следующим
образом:
Однако при простом откате положения нижней фигуры
положение линий может уже не стать прежним:
Предпосылки использования
Открытого интерфейса RelationSolver иногда не хватает
для точного отката всех изменений смежных объектов.
Механизм отката должен работать в тесном
взаимодействии с RelationSolver для восстановления
предыдущего состояния, но необходимо также
позаботиться о том, чтобы внутренние детали
RelationSolver не были доступны этому механизму.
Паттерн хранитель поможет решить данную проблему.
Хранитель — это объект, в котором сохраняется
внутреннее состояния другого объекта — хозяина
хранителя. В данном примере в качестве хозяина должен
выступить объект RelationSolver.
Диаграмма классов
Участники

Memento — хранитель.
Сохраняет внутреннее состояние объекта Originator.
Объем сохраняемой информации может быть
различным и определяется потребностями хозяина.
Запрещает доступ всем другим объектам, кроме
хозяина (по существу, у хранителей есть два
интерфейса).

Originator — хозяин.
Создает хранитель, содержащий снимок текущего
внутреннего состояния. Использует хранитель для
восстановления внутреннего состояния.

CareTaker — посыльный.
Отвечает за сохранение хранителя. Не производит
никаких операций над хранителем и не исследует его
внутреннее содержимое.
Плюсы и минусы




Сохранение границ инкапсуляции.
Хранитель позволяет избежать раскрытия информации, которой
должен распоряжаться только хозяин. Этот паттерн экранирует
объекты от потенциально сложного внутреннего устройства
хозяина, не изменяя границы инкапсуляции.
Упрощение структуры хозяина.
При других вариантах дизайна, направленного на сохранение
границ инкапсуляции, хозяин хранил бы внутри себя версии
внутреннего состояния, которое запрашивали клиенты. Таким
образом, вся ответственность за управление памятью лежала бы
на хозяине.
Значительные издержки при использовании хранителей.
С хранителями могут быть связаны заметные издержки, если
хозяин должен копировать большой объем информации или если
клиенты создают и возвращают хранителей достаточно часто.
Если плата за инкапсуляцию и восстановление состояния хозяина
велика, то этот паттерн не всегда подходит.
Определение «узкого» и «широкого» интерфейсов.
В некоторых языках сложно гарантировать, что только хозяин
имеет доступ к состоянию хранителя.
Пример №1
Вернѐмся к приложению графического редактора и
рассмотрим пример применения паттерна Хранитель.
Класс SolverMemento просто хранит положения всех
связей:
public class SolverMemento
{
private Dictionary<Relation, List<Point>> paths =
new Dictionary<Relation, List<Point>>();
public Dictionary<Relation, List<Point>> Paths
{
get { return paths; }
}
}
Пример №1
Реализация методов сохранения и восстановления
будет следующей:
public class RelationSolver
{
public SolverMemento CreateMemento()
{
SolverMemento memento = new SolverMemento();
foreach (KeyValuePair<Relation, List<Point>> pair in paths)
(memento.Paths as
ICollection<KeyValuePair<Relation, List<Point>>>).Add(pair);
return memento;
}
public void SetMemento(SolverMemento memento)
{
paths.Clear();
foreach (KeyValuePair<Relation, List<Point>> pair in memento.Paths)
(paths as
ICollection<KeyValuePair<Relation, List<Point>>>).Add(pair);
}
}
«Узкий» и «широкий» интерфейсы
Обратим внимание, что класс
SolverMemento имеет открытый интерфейс,
который раскрывает детали реализации
класса RelationSolver.
Некоторые языки позволяют обеспечить
«узкий» и «широкий» интерфейсы для
класса SolverMemeto с помощью следующих
механизмов:
 Java: вложенные классы,
 С++: дружественные классы.
Пример №2
Реализация класса RelationSolver на языке Java могла
бы быть следующей:
public class RelationSolver
{
private Map<Relation, List<Point>> paths;
public class SolverMemento
{
private Map<Relation, List<Point>> paths;
// Доступ к закрытым полям обеспечивается вложенностью класса
private Map<Relation, List<Point>> GetPaths()
{
return paths;
}
}
// ...
}
Пример №2
public class RelationSolver
{
// ...
public SolverMemento CreateMemento()
{
SolverMemento memento = new SolverMemento();
memento.GetPaths().putAll(paths);
return memento;
}
public void SetMemento(SolverMemento memento)
{
paths.clear();
paths.putAll(memento.GetPaths());
}
}
Пример №2
Реализации классов RelationSolver и SolverMemento на языке C++
могли бы быть следующими:
class SolverMemento;
class RelationSolver {
private:
map<Relation, vector<Point>> paths;
public:
SolverMemento * CreateMemento() const;
void SetMemento(const SolverMemento * memento);
};
class SolverMemento {
public:
virtual ~SolverMemento();
private:
friend class RelationSolver;
map<Relation, vector<Point>> paths;
SolverMemento();
};
Пример №3
Рассмотрим теперь механизм сериализации
в платформе .NET. Для обеспечения
возможности сериализации класса
необходимо:
 объявить класс с атрибутом [Serializable];
 объявить в классе открытый конструктор
без параметров;
 обеспечить сериализуемость всех его
открытых полей и свойств.
Пример №3
Обобщенный класс Memento, использующий сериализацию
объектов в поток двоичных данных, будет выглядеть следующим
образом:
public class Memento {
MemoryStream stream = new MemoryStream();
BinaryFormatter formatter = new BinaryFormatter();
public Memento Save(params object[] objects) {
foreach (object value in objects)
formatter.Serialize(stream, value);
return this;
}
public IEnumerable<object> Restore() {
stream.Seek(0, SeekOrigin.Begin);
while (stream.Position < stream.Length)
yield return formatter.Deserialize(stream);
stream.Close();
}
}
Пример №3
Рассмотрим применение указанной техники на примере
класса Rectangle:
public class Rectangle
{
public Rectangle(Point leftTop, Size size)
{
LeftTop = leftTop;
Size = size;
}
public Point LeftTop { get; set; }
public Size Size { get; set; }
}
Отметим, что библиотечные классы Point и Size
являются сериализуемыми.
Пример №3
Реализация методов сохранения и восстановления
будет следующей:
public class Rectangle
{
public Memento CreateMemento()
{
Memento result = new Memento();
result.Save(LeftTop, Size);
return result;
}
public void SetMemento(Memento memento)
{
List<object> restored = new List<object>(memento.Restore());
LeftTop = (Point)restored[0];
Size = (Size)restored[1];
}
}
Пример №3
Использование подобной техники может
облегчить возможность передачи состояния
объектов по сети или сохранения состояния
объектов между запусками приложения
(путѐм хранения состояния в файле)
Download