Справочник от Автор24
Поделись лекцией за скидку на Автор24

Типовые решения для объекто-ориентированных программ. Паттерны (patterns) проектирования

  • 👀 652 просмотра
  • 📌 578 загрузок
Выбери формат для чтения
Статья: Типовые решения для объекто-ориентированных программ. Паттерны (patterns) проектирования
Найди решение своей задачи среди 1 000 000 ответов
Загружаем конспект в формате pdf
Это займет всего пару минут! А пока ты можешь прочитать работу в формате Word 👇
Конспект лекции по дисциплине «Типовые решения для объекто-ориентированных программ. Паттерны (patterns) проектирования» pdf
ЛЕКЦИЯ 10 Тема: Типовые решения для объекто-ориентированных программ Паттерны (patterns) проектирования. Литература: Паттерны проектирования – это обобщенные решения проблем программного проектирования. Объектно-ориентированные паттерны проектирования обычно показывают отношения и взаимодействия между классами и объектами без специфики классов или объектов окончательного приложения. Паттерн проектирования – это описание задачи, которая постоянно возникает в ходе проектирования объектно-ориентированных программ, и принципов ее решения. Причем данное решение может быть использовано повторно. Смысл паттерна – предложить решение задачи в определенных условиях. В сравнении с полностью самостоятельным проектированием, паттерны обладают рядом преимуществ. Основная польза от использования паттернов состоит в снижении сложности разработки за счёт готовых абстракций для решения целого класса проблем. Паттерн даёт решению свое имя, что облегчает коммуникацию между разработчиками, позволяя ссылаться на известные паттерны. Таким образом, за счёт паттернов производится унификация деталей решений: модулей, элементов проекта, — снижается количество ошибок. Применение паттернов концептуально сродни использованию готовых библиотек кода. Правильно сформулированный паттерн проектирования позволяет, отыскав удачное решение, пользоваться им снова и снова. Набор паттернов помогает разработчику выбрать возможный, наиболее подходящий вариант проектирования В литературе по ИТ-паттернам паттерн описывается как нечто, состоящее из четырех основных элементов: • • • • Имя. Сославшись на имя, можно сразу описать проблему, ее решения и последствия. Присваивание паттернам имен позволяет проектировать на более высоком уровне абстракции. С помощью словаря паттернов можно вести обсуждение с коллегами, упоминать паттерны в документации, представлять тонкости системы. Задача. Описание того, когда следует применять паттерн. Формулируется задача и ее контекст. (Примером конкретной задачи может служить способ представления алгоритмов в виде объектов). Иногда в описание задачи входит перечень условий, при выполнении которых имеет смысл применять паттерн. Решение. Описание элементов решения (элементов проектирования, анализа, тестирования и др. в зависимости от вида паттерна), отношений между ними, функций каждого элемента. При этом решение - не конкретный дизайн или реализация, так как паттерн применяется в самых разных контекстах. Просто дается абстрактное описание задачи и того, как она может быть решена с помощью некоего весьма общего сочетания элементов (в случае проектирования, например, это могут быть объекты и классы). Результаты. Результаты - это следствия применения паттерна и разного рода компромиссы. Хотя при описании решений о последствиях часто не упоминают, знать о них необходимо, чтобы можно было выбирать между различными вариантами и оценивать преимущества и недостатки конкретного паттерна. Иногда в результатах может быть описан выбор языка и реализации. В случае проектирования к результатам относят влияние на степень гибкости, расширяемости и переносимости системы. Перечисление всех последствий помогает понять и оценить их роль. Паттерн проектирования — это описание взаимодействия объектов и классов , адаптированных для решения общей задачи проектирования в конкретном контексте. 1 Паттерн проектирования именует, абстрагирует и идентифицирует ключевые аспекты структуры общего решения, которые и позволяют применить его для создания повторно используемого дизайна. Он вычленяет участвующие классы и экземпляры, их роли и отношения, а также функции. Прото-паттерны Не всякое решение, алгоритм, лучшие практики или эвристики могут быть названы паттерном. Один или несколько признаков паттерна могут отсутствовать. Даже если все признаки паттерна есть, но описанная проблема и/или решение не являются повторяющимися, предлагаемую идею нельзя назвать паттерном. Существует мнение, что такой находящийся в разработке паттерн (прото-паттерн) должен быть применен по крайней мере трижды, просмотрен и одобрен значительным числом пользователей паттернов. Краткое описание прото-паттерна называется patlet. Анти-паттерны В то время как обычный паттерн описывает положительный опыт решения задачи, анти-паттерны описывают отрицательный опыт. Антипаттерны появились в середине 1990-х и не так многочисленны, как обычные паттерны. Под анти-паттернами понимают: 1. паттерны, описывающие плохое решение задачи, которое привело к печальным последствиям; 2. паттерны, описывающие выход из этого печального состояния и переход к хорошему решению. Мы кратко рассмотрим следующие типы паттернов проектирования: Порождающие паттерны Структурные паттерны Паттерны поведения Паттерны проектирования № Название паттерна Перевод Назначение паттерна Порождающие паттерны 1 Factory Method Фабричный метод 2 Abstract Factory Абстрактная фабрика 3 Singleton Одиночка 4 Prototype Прототип 5 Builder Строитель Определяет интерфейс для разработки объектов, при этом объекты данного класса могут быть созданы его подклассами. Предоставляет интерфейс для создания множества связанных между собой или независимых объектов, конкретные классы которых неизвестны. Для выбранного класса обеспечивает выполнение требования единственности экземпляра и предоставления к нему полного доступа. Описывает виды создаваемых объектов с помощью прототипа, что позволяет создавать новые объекты путем копирования этого прототипа. Отделяет создание сложного объекта от его представления, позволяя использовать один и тот же процесс разработки для создания различных представлений. Структурные паттерны 1 Adapter(синоним Адаптер (Обертка) - Wrapper) 2 Decorator Декоратор Преобразует существующий интерфейс класса в другой интерфейс, который понятен клиентам. При этом обеспечивает совместную работу классов, невозможную без данного паттерна из-за несовместимости интерфейсов. Применяется для расширения имеющейся функциональности и является альтернативой порождению подклассов на основе 2 3 Proxy Заместитель 4 Composite Компоновщик 5 Bridge Мост 6 Flyweight Приспособленец 7 Facade Фасад динамического назначения объектам новых операций. Подменяет выбранный объект другим объектом для управления контроля доступа к исходному объекту. Группирует объекты в иерархические структуры для представления отношений типа "часть-целое", что позволяет клиентам работать с единичными объектами так же, как с группами объектов. Отделяет абстракцию класса от его реализации, благодаря чему появляется возможность независимо изменять то и другое. Использует принцип разделения для эффективной поддержки большого числа мелких объектов. Предоставляет единый интерфейс к множеству операций или интерфейсов в системе на основе унифицированного интерфейса для облегчения работы с системой. Паттерны поведения of Цепочка обязанностей 1 Chain Responsibility 2 Command Команда 3 Interpreter Интерпретатор 4 Iterator Итератор 5 Mediator Посредник 6 Memento Хранитель 7 Observer Наблюдатель 8 State Состояние 9 Strategy Стратегия 10 Template Method Шаблонный метод 11 Visitor Посетитель Позволяет избежать жесткой зависимости отправителя запроса от его получателя, при этом объекты-получатели связываются в цепочку, а запрос передается по цепочке, пока какой-то объект его не обработает. Инкапсулирует запрос в виде объекта, обеспечивая параметризацию клиентов типом запроса, установление очередности запросов, протоколирование запросов и отмену выполнения операций. Для заданного языка определяет представление его грамматики на основе интерпретатора предложений языка, использующего это представление. Дает возможность последовательно перебрать все элементы составного объекта, не раскрывая его внутреннего представления. Определяет объект, в котором инкапсулировано знание о том, как взаимодействуют объекты из некоторого множества. Способствует уменьшению числа связей между объектами, позволяя им работать без явных ссылок друг на друга и независимо изменять схему взаимодействия. Дает возможность получить и сохранить во внешней памяти внутреннее состояние объекта, чтобы позже объект можно было восстановить точно в таком же состоянии, не нарушая принципа инкапсуляции. Специфицирует зависимость типа "один ко многим" между различными объектами, так что при изменении состояния одного объекта все зависящие от него получают извещение и автоматически обновляются. Позволяет выбранному объекту варьировать свое поведение при изменении внутреннего состояния. При этом создается впечатление, что изменился класс объекта. Определяет множество алгоритмов, инкапсулируя их все и позволяя подставлять один вместо другого. При этом можно изменять алгоритм независимо от клиента, который им пользуется. Определяет структуру алгоритма, перераспределяя ответственность за некоторые его шаги на подклассы. При этом подклассы могут переопределять шаги алгоритма, не меняя его общей структуры. Позволяет определить новую операцию, не меняя описаний классов, у объектов которых она вызывается. Порождающие паттерны 3 Порождающие паттерны проектирования предназначены для создания объектов, позволяя системе оставаться независимой как от самого процесса порождения, так и от типов порождаемых объектов. Паттерн Factory Method (фабричный метод) Назначение паттерна Factory Method В системе часто требуется создавать объекты самых разных типов. Паттерн Factory Method (фабричный метод) может быть полезным в решении следующих задач: • • Система должна оставаться расширяемой путем добавления объектов новых типов. Непосредственное использование выражения new является нежелательным, так как в этом случае код создания объектов с указанием конкретных типов может получиться разбросанным по всему приложению. Тогда такие операции как добавление в систему объектов новых типов или замена объектов одного типа на другой будут затруднительными. Паттерн Factory Method позволяет системе оставаться независимой как от самого процесса порождения объектов, так и от их типов. Заранее известно, когда нужно создавать объект, но неизвестен его тип. Описание паттерна Factory Method Для того, чтобы система оставалась независимой от различных типов объектов, паттерн Factory Method использует механизм полиморфизма - классы всех конечных типов наследуют от одного абстрактного базового класса, предназначенного для полиморфного использования. В этом базовом классе 4 определяется единый интерфейс, через который пользователь будет оперировать объектами конечных типов. Для обеспечения относительно простого добавления в систему новых типов паттерн Factory Method локализует создание объектов конкретных типов в специальном классе-фабрике. Методы этого класса, посредством которых создаются объекты конкретных классов, называются фабричными. Существуют две разновидности паттерна Factory Method: Обобщенный конструктор, когда в том же самом полиморфном базовом классе, от которого наследуют производные классы всех создаваемых в системе типов, определяется статический фабричный метод. В качестве параметра в этот метод должен передаваться идентификатор типа создаваемого объекта. UML-диаграмма классов паттерна Factory Method. Обобщенный конструктор Классический вариант фабричного метода, когда интерфейс фабричных методов объявляется в независимом классе-фабрике, а их реализация определяется конкретными подклассами этого класса. UML-диаграмма классов паттерна Factory Method. Классическая реализация #include #include #include #include "pch.h" using namespace std; enum Car_ID { Vaz_ID = 0, Uaz_ID, Truck_ID }; 5 class Car { public: virtual void info() = 0; virtual ~Car() {} // Параметризированный статический фабричный метод static Car * createCar(Car_ID id); }; class Vaz : public Car { public: void info() { cout << "ВАЗ 2115" << endl; } }; class Uaz : public Car { public: void info() { cout << "УАЗ-3163 Патриот" << endl; } }; class Truck : public Car { public: void info() { cout << "Грузовик" << endl; } }; // Реализация параметризированного фабричного метода Car * Car::createCar(Car_ID id) { Car * ptrCar=nullptr; switch (id) { case Vaz_ID: ptrCar = new Vaz(); break; case Uaz_ID: ptrCar = new Uaz(); break; case Truck_ID: ptrCar = new Truck(); break; default: assert(false); } return ptrCar; }; // Создание объектов при помощи параметризированного фабричного метода int main() { setlocale(LC_ALL, "rus"); vector v; v.push_back(Car::createCar(Vaz_ID)); v.push_back(Car::createCar(Uaz_ID)); v.push_back(Car::createCar(Truck_ID)); // for (int i = 0; iinfo(); } 6 Представленный вариант паттерна Factory Method пользуется популярностью благодаря своей простоте. В нем статический фабричный метод createCar() определен непосредственно в полиморфном базовом классе Car. Этот фабричный метод является параметризированным, то есть для создания объекта некоторого типа в createCar() передается соответствующий идентификатор типа. С точки зрения "чистоты" объектно-ориентированного кода у этого варианта есть следующие недостатки: • • Так как код по созданию объектов всех возможных типов сосредоточен в статическом фабричном методе класса Car, то этот базовый класс обладает знанием обо всех производных от него классах, что является нетипичным для объектно-ориентированного подхода. Подобное использование оператора switch (как в коде фабричного метода createCar()) в объектноориентированном программировании также не приветствуется. Указанные недостатки отсутствуют в классической реализации паттерна Factory Method. #include "pch.h" // ConsoleApplication53.cpp : Классическая реализация паттерна Factory Method // #include #include using namespace std; // Иерархия классов игровых персонажей class Car { public: virtual void info() = 0; virtual ~Car() {} }; class Vaz : public Car { public: void info() { cout << "ВАЗ 2115" << endl; } }; class Uaz : public Car { public: void info() { cout << "УАЗ-3163 Патриот" << endl; } }; class Truck : public Car { public: void info() { cout << "Грузовик" << endl; } }; // Фабрики объектов class Factory { public: virtual Car * createCar() = 0; virtual ~Factory() {} }; 7 class VazFactory : public Factory { public: Car* createCar() { return new Vaz; } }; class UazFactory : public Factory { public: Car * createCar() { return new Uaz; } }; class TruckFactory : public Factory { public: Car* createCar() { return new Truck; } }; // Создание объектов при помощи фабрик объектов int main() { setlocale(LC_ALL, "rus"); VazFactory *vaz_factory = new VazFactory; UazFactory* uaz_factory = new UazFactory; TruckFactory* truck_factory = new TruckFactory; vector v; v.push_back(vaz_factory->createCar()); v.push_back(uaz_factory->createCar()); v.push_back(uaz_factory->createCar()); //foreach (Car * p in v) for(Car *p:v) { p->info(); delete p; } } Классический вариант паттерна Factory Method использует идею полиморфной фабрики. Специально выделенный для создания объектов полиморфный базовый класс Factory объявляет интерфейс фабричного метода createCar(), а производные классы его реализуют. Представленный вариант паттерна Factory Method является наиболее распространенным, но не единственным. Возможны следующие вариации: • • Класс Factory имеет реализацию фабричного метода createCar() по умолчанию. Фабричный метод createCar()класса Factory параметризирован типом создаваемого объекта (как и у представленного ранее, простого варианта Factory Method) и имеет реализацию по умолчанию. В 8 этом случае, производные от Factory классы необходимы лишь для того, чтобы определить нестандартное поведение createCar(). Результаты применения паттерна Factory Method Достоинства паттерна Factory Method • Создает объекты разных типов, позволяя системе оставаться независимой как от самого процесса создания, так и от типов создаваемых объектов. Недостатки паттерна Factory Method • В случае классического варианта паттерна даже для порождения единственного объекта необходимо создавать соответствующую фабрику. Паттерн Abstract Factory (абстрактная фабрика) Использует несколько фабричных методов и предназначен для создания целого семейства или группы взаимосвязанных объектов. Назначение паттерна Abstract Factory Используйте паттерн Abstract Factory (абстрактная фабрика) если: • • Система должна оставаться независимой как от процесса создания новых объектов, так и от типов порождаемых объектов. Непосредственное использование выражения new в коде приложения нежелательно. Необходимо создавать группы или семейства взаимосвязанных объектов, исключая возможность одновременного использования объектов из разных семейств в одном контексте. Приведем примеры групп взаимосвязанных объектов. Пусть некоторое приложение с поддержкой графического интерфейса пользователя рассчитано на использование на различных платформах, при этом внешний вид этого интерфейса должен соответствовать принятому стилю для той или иной платформы. Например, если это приложение установлено на Windows-платформу, то его кнопки, меню, полосы прокрутки должны отображаться в стиле, принятом для Windows. Группой взаимосвязанных объектов в этом случае будут элементы графического интерфейса пользователя для конкретной платформы. Другой пример. Рассмотрим текстовый редактор с многоязычной поддержкой, у которого имеются функциональные модули, отвечающие за расстановку переносов слов и проверку орфографии. Если, скажем, открыт документ на русском языке, то должны быть подключены соответствующие модули, учитывающие специфику русского языка. Ситуация, когда для такого документа одновременно используются модуль расстановки переносов для русского языка и модуль проверки орфографии для немецкого языка, исключается. Здесь группой взаимосвязанных объектов будут соответствующие модули, учитывающие специфику некоторого языка. Описание паттерна Abstract Factory Паттерн Abstract Factory реализуется на основе фабричных методов (см. паттерн Factory Method). Любое семейство или группа взаимосвязанных объектов характеризуется несколькими общими типами создаваемых продуктов, при этом сами продукты таких типов будут различными для разных 9 семейств. Для того чтобы система оставалась независимой от специфики того или иного семейства продуктов необходимо использовать общие интерфейсы для всех основных типов продуктов. Для решения задачи по созданию семейств взаимосвязанных объектов паттерн Abstract Factory вводит понятие абстрактной фабрики. Абстрактная фабрика представляет собой некоторый полиморфный базовый класс, назначением которого является объявление интерфейсов фабричных методов, служащих для создания продуктов всех основных типов (один фабричный метод на каждый тип продукта). Производные от него классы, реализующие эти интерфейсы, предназначены для создания продуктов всех типов внутри семейства или группы. #include "pch.h" // ConsoleApplication54.cpp : Реализация паттерна Abstract Factory // //#include "stdafx.h" #include #include using namespace std; // Абстрактные базовые классы всех возможных видов машин class Vaz { public: virtual void info() = 0; virtual ~Vaz() {} }; class Uaz { public: virtual void info() = 0; virtual ~Uaz() {} }; 10 class Truck { public: virtual void info() = 0; virtual ~Truck() {} }; // Абстрактная фабрика для производства машин class CarFactory { public: virtual Vaz* createVaz() = 0; virtual Uaz* createUaz() = 0; virtual Truck* createTruck() = 0; virtual ~CarFactory() {} }; // Классы всех видов машин таксопарка class TaxiVaz : public Vaz { public: void info() { cout << "Ваз - такси" << endl; } }; class TaxiUaz : public Uaz { public: void info() { cout << "УАЗ - такси" << endl; } }; class TaxiTruck : public Truck { public: void info() { cout << "Грузовик - такси" << endl; } }; // Классы всех видов машин автоколонны class ParkVaz : public Vaz { public: void info() { cout << "Ваз - автоколонна" << endl; } }; class ParkUaz : public Uaz { public: void info() { cout << "Уаз - автоколонна" << endl; } }; class ParkTruck : public Truck { public: void info() { cout << "Грузовик - автоколонна" << endl; } }; // Фабрика для создания машин таксопарка class TaxiFactory : public CarFactory { public: Vaz* createVaz() { return new TaxiVaz; } Uaz* createUaz() { return new TaxiUaz; } Truck* createTruck() { return new TaxiTruck; } }; 11 // Фабрика для создания машин автоколонны class ParkFactory : public CarFactory { public: Vaz* createVaz() { return new ParkVaz; } Uaz* createUaz() { return new ParkUaz; } Truck* createTruck() { return new ParkTruck; } }; // Класс, содержащий все машины предприятия class Company { public: ~Company() { for (Vaz *p : vv) delete p; for (Uaz *p : vu) delete p; for (Truck *p : vt) delete p; } void info() { for (Vaz *p : vv) p->info(); for (Uaz *p : vu) p->info(); for (Truck *p : vt) p->info(); } vector vv; vector vu; vector vt; }; // Здесь создаются предприятия class Holding { public: Company* createCompany(CarFactory& factory, int vs, int us, int ts = 0) { int i; Company* p = new Company; for (i = 0; i < vs; i++) p->vv.push_back(factory.createVaz()); for (i = 0; i < us; i++) p->vu.push_back(factory.createUaz()); for (i = 0; i < ts; i++) p->vt.push_back(factory.createTruck()); return p; } }; int main() { Holding hold; TaxiFactory t_factory; ParkFactory p_factory; setlocale(LC_ALL, "rus"); Company * tx = hold.createCompany(t_factory, 3, 1, 0); Company * pk = hold.createCompany(p_factory, 0, 2, 3); cout << "Таксопарк:" << endl; tx->info(); cout << endl << "автоколонна:" << endl; pk->info(); } 12 Результаты применения паттерна Abstract Factory Достоинства паттерна Abstract Factory • • Скрывает сам процесс порождения объектов, а также делает систему независимой от типов создаваемых объектов, специфичных для различных семейств или групп (пользователи оперируют этими объектами через соответствующие абстрактные интерфейсы). Позволяет быстро настраивать систему на нужное семейство создаваемых объектов. В случае многоплатформенного графического приложения для перехода на новую платформу, то есть для замены графических элементов (кнопок, меню, полос прокрутки) одного стиля другим достаточно создать нужный подкласс абстрактной фабрики. При этом условие невозможности одновременного использования элементов разных стилей для некоторой платформы будет выполнено автоматически. Недостатки паттерна Abstract Factory • Трудно добавлять новые типы создаваемых продуктов или заменять существующие, так как интерфейс базового класса абстрактной фабрики фиксирован. Например, если для нашей стратегической игры нужно будет ввести новый вид военной единицы - осадные орудия, то надо будет добавить новый фабричный метод, объявив его интерфейс в полиморфном базовом классе AbstractFactory и реализовав во всех подклассах. Снять это ограничение можно следующим образом. Все создаваемые объекты должны наследовать от общего абстрактного базового класса, а в единственный фабричный метод в качестве параметра необходимо передавать идентификатор типа объекта, который нужно создать. Однако в этом случае необходимо учитывать следующий момент. Фабричный метод создает объект запрошенного подкласса, но при этом возвращает его с интерфейсом общего абстрактного класса в виде ссылки или указателя, поэтому для такого объекта будет затруднительно выполнить какую-либо операцию, специфичную для подкласса. 13 Паттерн Builder (строитель) Определяет процесс поэтапного конструирования сложного объекта, в результате которого могут получаться разные представления этого объекта. Назначение паттерна Builder Паттерн Builder может помочь в решении следующих задач: • • В системе могут существовать сложные объекты, создание которых за одну операцию затруднительно или невозможно. Требуется поэтапное построение объектов с контролем результатов выполнения каждого этапа. Данные должны иметь несколько представлений. Приведем классический пример. Пусть есть некоторый исходный документ в формате RTF (Rich Text Format), в общем случае содержащий текст, графические изображения и служебную информацию о форматировании (размер и тип шрифтов, отступы и др.). Если этот документ в формате RTF преобразовать в другие форматы (например, Microsoft Word или простой ASCII-текст), то полученные документы и будут представлениями исходных данных. Описание паттерна Builder Паттерн Builder отделяет алгоритм поэтапного конструирования сложного продукта (объекта) от его внешнего представления так, что с помощью одного и того же алгоритма можно получать разные представления этого продукта. Поэтапное создание продукта означает его построение по частям. После того как построена последняя часть, продукт можно использовать. Для этого паттерн Builder определяет алгоритм поэтапного создания продукта в специальном классе Director (распорядитель), а ответственность за координацию процесса сборки отдельных частей продукта возлагает на иерархию классов Builder. В этой иерархии базовый класс Builder объявляет интерфейсы для построения отдельных частей продукта, а соответствующие подклассы ConcreteBuilder их реализуют подходящим образом, например, создают или получают нужные ресурсы, сохраняют промежуточные результаты, контролируют результаты выполнения операций. Класс Director содержит указатель или ссылку на Builder, который перед началом работы должен быть сконфигурирован экземпляром ConcreteBuilder, определяющим соответствующее представление. После этого Director может обрабатывать клиентские запросы на создание объекта. Получив такой запрос, с помощью имеющегося экземпляра строителя Director строит продукт по частям, а затем возвращает его пользователю. UML-диаграмма последовательности паттерна Builder 14 Для получения разных представлений некоторых данных с помощью паттерна Builder распорядитель Director должен использовать соответствующие экземпляры ConcreteBuilder. 15 #include "pch.h" // ConsoleApplication55.cpp : Defines the entry point for the console application. // #include #include using namespace std; // Классы всех возможных машин class Vaz { public: void info() { cout << "ВАЗ 2115" << endl; } }; 16 class Uaz { public: void info() { cout << "УАЗ-3163 Патриот" << endl; } }; class Limo { public: void info() { cout << "Лимузин" << endl; } }; class Truck { public: void info() { cout << "Грузовик" << endl; } }; class Tractor { public: void info() { cout << "Трактор" << endl; } }; // Класс "Предприятие", содержащий все типы машин class Company { public: vector vv; vector vu; vector vl; vector vt; vector vtr; void info() { for (Vaz v : vv) v.info(); for (Uaz v : vu) v.info(); for (Limo v : vl) v.info(); for (Truck v : vt) v.info(); for (Tractor v : vtr) v.info(); } }; // Базовый класс CompanyBuilder объявляет интерфейс для поэтапного // построения предприятия и предусматривает его реализацию по умолчанию class CompanyBuilder { protected: Company* p; public: CompanyBuilder() : p(0) {} virtual ~CompanyBuilder() {} virtual void createCompany() {} virtual void buildVaz() {} virtual void buildUaz() {} virtual void buildLimo() {} virtual void buildTruck() {} virtual void buildTractor() {} virtual Company* getCompany() { return p; } }; // таксопарк имеет все кроме грузовиков и тракторов 17 class TaxiBuilder : public CompanyBuilder { public: void createCompany() { p = new Company; } void buildVaz() { p->vv.push_back(Vaz()); } void buildUaz() { p->vu.push_back(Uaz()); } void buildLimo() { p->vl.push_back(Limo()); } }; // Автоколонна имеет все кроме ВАЗ и лимузинов class ParkBuilder : public CompanyBuilder { public: void createCompany() { p = new Company; } void buildUaz() { p->vu.push_back(Uaz()); } void buildTruck() { p->vt.push_back(Truck()); } void buildTractor() { p->vtr.push_back(Tractor()); } }; // Класс-распорядитель, поэтапно создающий таксопарк или автоколонну // Именно здесь определен алгоритм построения компании class Director { public: Company* createCompany(CompanyBuilder & builder) { builder.createCompany(); builder.buildVaz(); builder.buildUaz(); builder.buildLimo(); builder.buildTruck(); builder.buildTractor(); return(builder.getCompany()); } }; int main() { Director dir; TaxiBuilder tx_builder; ParkBuilder pk_builder; Company *tx = dir.createCompany(tx_builder); Company *pk = dir.createCompany(pk_builder); setlocale(LC_ALL, "rus"); cout << "Таксопарк" << endl; tx->info(); cout << endl << "Автоколонна:" << endl; pk->info(); return 0; } Очень часто базовый класс строителя (в коде выше это CompanyBuilder) не только объявляет интерфейс для построения частей продукта, но и определяет ничего не делающую 18 реализацию по умолчанию. Тогда соответствующие подклассы (TaxiBuilder, ParkBuilder) переопределяют только те методы, которые участвуют в построении текущего объекта. Так класс TaxiBuilder не определяет методы buildTruck() и buildTractor(), поэтому таксопарк не может иметь грузовики и тракторы. А в классе ParkBuilder не определены buidLimo() и buildVaz(), поэтому автоколонна не может иметь ВАЗ и лимузин. Интересно сравнить приведенный код с кодом создания компании в реализации паттерна Abstract Factory, который также может использоваться для создания сложных продуктов. Если паттерн Abstract Factory акцентирует внимание на создании семейств некоторых объектов, то паттерн Builder подчеркивает поэтапное построение продукта. При этом класс Builder скрывает все подробности построения сложного продукта так, что класс Director ничего не знает о его составных частях. Результаты применения паттерна Builder Достоинства паттерна Builder • Возможность контролировать процесс создания сложного продукта. • Возможность получения разных представлений некоторых данных. Недостатки паттерна Builder • ConcreteBuilder и создаваемый им продукт жестко связаны между собой, поэтому при внесении изменений в класс продукта скорее всего придется соответствующим образом изменять и класс ConcreteBuilder. Паттерн Prototype (прототип) Создает новые объекты с помощью прототипов. Прототип - некоторый объект, умеющий создавать по запросу копию самого себя. Назначение паттерна Prototype Паттерн Prototype (прототип) можно использовать в следующих случаях: • Система должна оставаться независимой как от процесса создания новых объектов, так и от типов порождаемых объектов. Непосредственное использование выражения 19 new в коде приложения считается нежелательным (подробнее об этом в разделе Порождающие паттерны). • Необходимо создавать объекты, точные классы которых становятся известными уже на стадии выполнения программы. Паттерн Factory Method также делает систему независимой от типов порождаемых объектов, но для этого он вводит параллельную иерархию классов: для каждого типа создаваемого объекта должен присутствовать соответствующий класс-фабрика, что может быть нежелательно. Паттерн Prototype лишен этого недостатка. Описание паттерна Prototype Для создания новых объектов паттерн Prototype использует прототипы. Прототип это уже существующий в системе объект, который поддерживает операцию клонирования, то есть умеет создавать копию самого себя. Таким образом, для создания объекта некоторого класса достаточно выполнить операцию clone() соответствующего прототипа. Паттерн Prototype реализует подобное поведение следующим образом: все классы, объекты которых нужно создавать, должны быть подклассами одного общего абстрактного базового класса. Этот базовый класс должен объявлять интерфейс метода clone(). Также здесь могут объявляться виртуальными и другие общие методы, например, initialize() в случае, если после клонирования нужна инициализация вновь созданного объекта. Все производные классы должны реализовывать метод clone(). В языке С++ для создания копий объектов используется конструктор копирования, однако, в общем случае, создание объектов при помощи операции копирования не является обязательным. Для порождения объекта некоторого типа в системе должен существовать его прототип. Прототип представляет собой объект того же типа, единственным назначением которого является создание подобных ему объектов. Обычно для удобства все существующие в системе прототипы организуются в специальные коллекции-хранилища или реестры прототипов. Такое хранилище может иметь реализацию в виде ассоциативного массива, каждый элемент которого представляет пару "Идентификатор типа" - "Прототип". Реестр прототипов позволяет добавлять или удалять прототип, а также создавать объект по идентификатору типа. Именно операции динамического добавления и удаления прототипов в хранилище обеспечивают дополнительную гибкость системе, позволяя управлять процессом создания новых объектов. Также как и для паттерна Factory Method приведем две возможные реализации паттерна Prototype, а именно: 1. В виде обобщенного конструктора на основе прототипов, когда в полиморфном базовом классе Prototype определяется статический метод, предназначенный для создания объектов. При этом в качестве параметра в этот метод должен передаваться идентификатор типа создаваемого объекта. App56 20 #include "pch.h" // ConsoleApplication56.cpp : Реализация паттерна Ptototype на основе обобщенного конструктора // #include #include #include using namespace std; // Идентификаторы всех машин enum Car_ID { Vaz_ID = 0, Uaz_ID, Truck_ID }; class Car; // Опережающее объявление typedef map Registry; // Реестр прототипов определен в виде Singleton Мэйерса Registry& getRegistry() { static Registry _instance; return _instance; } // Единственное назначение этого класса - помощь в выборе нужного // конструктора при создании прототипов class Dummy { }; // Полиморфный базовый класс. Здесь также определен статический // обобщенный конструктор для создания экземпляров всех машин class Car { public: virtual Car* clone() = 0; virtual void info() = 0; virtual ~Car() {} // Параметризированный статический метод для создания любой машины static Car* createCar(Car_ID id) { Registry& r = getRegistry(); if (r.find(id) != r.end()) return r[id]->clone(); return 0; } protected: // Добавление прототипа в множество прототипов static void addPrototype(Car_ID id, Car * prototype) { Registry& r = getRegistry(); r[id] = prototype; 21 } // Удаление прототипа из множества прототипов static void removePrototype(Car_ID id) { Registry& r = getRegistry(); r.erase(r.find(id)); } }; // В производных классах различных машин в виде статических // членов-данных определяются соответствующие прототипы class Vaz : public Car { public: Car* clone() { return new Vaz(*this); } void info() { cout << "ВАЗ 2115" << endl; } private: Vaz(Dummy) { Car::addPrototype(Vaz_ID, this); } Vaz() {} static Vaz prototype; }; class Uaz : public Car { public: Car* clone() { return new Uaz(*this); } void info() { cout << "УАЗ-3163 Патриот" << endl; } private: Uaz(Dummy) { addPrototype(Uaz_ID, this); } Uaz() {} static Uaz prototype; }; class Truck : public Car { public: Car* clone() { return new Truck(*this); } void info() { cout << "Грузовик" << endl; } private: Truck(Dummy) { addPrototype(Truck_ID, this); } Truck() {} static Truck prototype; }; Vaz Vaz::prototype = Vaz(Dummy()); Uaz Uaz::prototype = Uaz(Dummy()); Truck Truck::prototype = Truck(Dummy()); int main() { setlocale(LC_ALL, "rus"); vector v; v.push_back(Car::createCar(Vaz_ID)); v.push_back(Car::createCar(Uaz_ID)); v.push_back(Car::createCar(Truck_ID)); for(Car *p: v) p->info(); } 22 2. На базе специально выделенного класса-фабрики. App57 #include "pch.h" // ConsoleApplication57.cpp : Реализация паттерна Prototype с помощью выделенного классафабрики // #include #include using namespace std; // Иерархия классов машин // Полиморфный базовый класс class Car { public: virtual Car* clone() = 0; virtual void info() = 0; virtual ~Car() {} }; // Производные классы различных типов машин class Vaz : public Car { friend class PrototypeFactory; public: Car* clone() { return new Vaz(*this); } void info() { cout << "ВАЗ 2115" << endl; } private: Vaz() {} }; class Uaz : public Car { friend class PrototypeFactory; public: Car* clone() { return new Uaz(*this); } void info() { cout << "УАЗ-3163 Патриот" << endl; } private: Uaz() {} }; class Truck : public Car { friend class PrototypeFactory; public: Car* clone() { return new Truck(*this); } void info() { cout << "Грузовик" << endl; } private: Truck() {} }; // Фабрика для создания экземпляров машин всех типов class PrototypeFactory { public: Car* createVaz() { static Vaz prototype; return prototype.clone(); } Car* createUaz() { static Uaz prototype; return prototype.clone(); } Car* createTruck() { static Truck prototype; return prototype.clone(); } }; 23 int main() { PrototypeFactory factory; vector v; setlocale(LC_ALL, "rus"); v.push_back(factory.createVaz()); v.push_back(factory.createUaz()); v.push_back(factory.createTruck()); for( Car *p: v) p->info(); } В приведенной реализации (на базе обобщенного конструктора) классы всех создаваемых машин являются подклассами абстрактного базового класса Car. В этом классе определен обобщенный конструктор в виде статического метода createCar(Car_ID id). Передавая в этот метод в качестве параметра тип машины, можно создавать машину нужного типа. Для этого обобщенный конструктор использует реестр прототипов, реализованный в виде ассоциативного массива std::map, каждый элемент которого представляет собой пару "идентификатор типа машины" - "её прототип". Добавление прототипов в реестр происходит автоматически. Сделано это следующим образом. В подклассах Vaz, Uaz, Truck, прототипы определяются в виде статических членов данных тех же типов. При создании такого прототипа будет вызываться конструктор с параметром типа Dummy, который и добавит этот прототип в реестр прототипов с помощью метода addPrototype() базового класса Car. Важно, чтобы к этому моменту сам объект реестра был полностью сконструирован, именно поэтому он выполнен в виде singleton Мэйерса. Для приведенной реализации паттерна Prototype можно отметить следующие особенности: • Создавать новые машины можно только при помощи обобщенного конструктора. Их непосредственное создание невозможно, так как соответствующие конструкторы объявлены со спецификатором доступа private. • Отсутствует недостаток реализации на базе обобщенного конструктора для паттерна Factory Method, а именно базовый класс Car ничего не знает о своих подклассах. В приведенной реализации для упрощения кода реестр прототипов не ведется. Машины всех типов создаются при помощи соответствующих методов фабричного класса PrototypeFactory, где и определены прототипы в виде статических переменных. Результаты применения паттерна Prototype Достоинства паттерна Prototype • Для создания новых объектов клиенту необязательно знать их конкретные классы. • Возможность гибкого управления процессом создания новых объектов за счет возможности динамических добавления и удаления прототипов в реестр. 24 Недостатки паттерна Prototype • Каждый тип создаваемого продукта должен реализовывать операцию клонирования clone(). В случае, если требуется глубокое копирование объекта (объект содержит ссылки или указатели на другие объекты), это может быть непростой задачей. Паттерн Singleton (одиночка,синглет) Назначение паттерна Singleton Контролирует создание единственного экземпляра некоторого класса и предоставляет доступ к нему Часто в системе могут существовать сущности только в единственном экземпляре, например, система ведения системного журнала сообщений или драйвер дисплея. В таких случаях необходимо уметь создавать единственный экземпляр некоторого типа, предоставлять к нему доступ извне и запрещать создание нескольких экземпляров того же типа. Паттерн Singleton предоставляет такие возможности. Описание паттерна Singleton Архитектура паттерна Singleton основана на идее использования глобальной переменной, имеющей следующие важные свойства: 1. Такая переменная доступна всегда. Время жизни глобальной переменной - от запуска программы до ее завершения. 2. Предоставляет глобальный доступ, то есть, такая переменная может быть доступна из любой части программы. Однако, использовать глобальную переменную некоторого типа непосредственно невозможно, так как существует проблема обеспечения единственности экземпляра, а именно, возможно создание нескольких переменных того же самого типа (например, стековых). Для решения этой проблемы паттерн Singleton возлагает контроль над созданием единственного объекта на сам класс. Доступ к этому объекту осуществляется через статическую функцию-член класса, которая возвращает указатель или ссылку на него. Этот объект будет создан только при первом обращении к методу, а все последующие вызовы просто возвращают его адрес. Для обеспечения уникальности объекта, конструкторы и оператор присваивания объявляются закрытыми. UML-диаграмма классов паттерна Singleton 25 Паттерн Singleton часто называют усовершенствованной глобальной переменной. Классическая реализация паттерна Singleton #include "pch.h" // ConsoleApplication58.cpp : Классическая реализация Singleton // //#include "stdafx.h" #include using namespace std; class Singleton { private: static Singleton * p_instance; // Конструкторы и оператор присваивания недоступны клиентам Singleton() {} Singleton(const Singleton&); Singleton& operator=(Singleton&); public: static Singleton * getInstance() { if (!p_instance) p_instance = new Singleton(); return p_instance; } }; Singleton* Singleton::p_instance = 0; int main() { Singleton *ps = Singleton::getInstance(); cout << ps << endl; return 0; } Клиенты запрашивают единственный объект класса через статическую функцию-член getInstance(), которая при первом запросе динамически выделяет память под этот объект и затем возвращает указатель на этот участок памяти. Впоследcтвии клиенты должны сами позаботиться об освобождении памяти при помощи оператора delete. Последняя особенность является серьезным недостатком классической реализации паттерна Singleton. Так как класс сам контролирует создание единственного объекта, было бы логичным возложить на него ответственность и за разрушение объекта. Этот недостаток отсутствует в реализации Singleton, впервые предложенной Скоттом Мэйерсом. // Singleton.h class Singleton { private: Singleton() {} Singleton(const Singleton&); Singleton& operator=(Singleton&); public: static Singleton& getInstance() { static Singleton instance; // !!!! статический член внутри функции return instance; } }; 26 Внутри getInstance() используется статический экземпляр нужного класса. Стандарт языка программирования C++ гарантирует автоматическое уничтожение статических объектов при завершении программы. Досрочного уничтожения и не требуется, так как объекты Singleton обычно являются долгоживущими объектами. Статическая функция-член getInstance() возвращает не указатель, а ссылку на этот объект, тем самым, затрудняя возможность ошибочного освобождения памяти клиентами. Внутри getInstance() используется статический экземпляр нужного класса. Стандарт языка программирования C++ гарантирует автоматическое уничтожение статических объектов при завершении программы. Досрочного уничтожения и не требуется, так как объекты Singleton обычно являются долгоживущими объектами. Статическая функция-член getInstance() возвращает не указатель, а ссылку на этот объект, тем самым, затрудняя возможность ошибочного освобождения памяти клиентами. Несмотря на кажущуюся простоту паттерна Singleton (используется всего один класс), его реализация не является тривиальной. Результаты применения паттерна Singleton Достоинства паттерна Singleton • Класс сам контролирует процесс создания единственного экземпляра. • Паттерн легко адаптировать для создания нужного числа экземпляров. • Возможность создания объектов классов, производных от Singleton. Недостатки паттерна Singleton • В случае использования нескольких взаимозависимых одиночек их реализация может резко усложниться. Структурные паттерны проектирования Структурные паттерны определяют отношения между классами и объектами, позволяя им работать совместно. При этом могут использоваться следующие механизмы: • Наследование, когда базовый класс определяет интерфейс, а подклассы - реализацию. Структуры на основе наследования получаются статичными. • Композиция, когда структуры строятся путем объединения объектов некоторых классов. Композиция позволяет получать структуры, которые можно изменять во время выполнения. Адаптер (Adapter) Паттерн Adapter представляет собой программную обертку над уже существующими классами и предназначен для преобразования их интерфейсов к виду, пригодному для последующего использования в новом программном проекте. 27 Назначение: (Задача) Часто в новом программном проекте не удается повторно использовать уже существующий код. Например, имеющиеся классы могут обладать нужной функциональностью, но иметь при этом несовместимые интерфейсы. В таких случаях следует использовать паттерн Adapter (адаптер). Паттерн Adapter, представляющий собой программную обертку над существующими классами, преобразует их интерфейсы к виду, пригодному для последующего использования. Пример: Чтобы заставить работать «советскую» вилку через евро-розетку требуется переходник. Именно это и делает «адаптер», служит промежуточным объектом между двумя другими, которые не могут работать напрямую друг с другом. Рассмотрим простой пример, когда следует применять паттерн Adapter. Пусть мы разрабатываем систему климат-контроля, предназначенной для автоматического поддержания температуры окружающего пространства в заданных пределах. Важным компонентом такой системы является температурный датчик, с помощью которого измеряют температуру окружающей среды для последующего анализа. Для этого датчика уже имеется готовое программное обеспечение от сторонних разработчиков, представляющее собой некоторый класс с соответствующим интерфейсом. Однако использовать этот класс непосредственно не удастся, так как показания датчика снимаются в градусах Фаренгейта. Нужен адаптер, преобразующий температуру в шкалу Цельсия. Описание паттерна Adapter (Решение) Пусть класс, интерфейс которого нужно адаптировать к нужному виду, имеет имя Adapteк. Для решения задачи преобразования его интерфейса паттерн Adapter вводит следующую иерархию классов: • Виртуальный базовый класс Target. Здесь объявляется пользовательский интерфейс подходящего вида. Только этот интерфейс доступен для пользователя. • Производный класс Adapter, реализующий интерфейс Target. В этом классе также имеется указатель или ссылка на экземпляр Adaptee. Паттерн Adapter использует этот указатель для перенаправления клиентских вызовов в Adaptee. Так как интерфейсы Adaptee и Target несовместимы между собой, то эти вызовы обычно требуют преобразования. UML-диаграмма классов паттерна Adapter 28 // ConsoleApplication40.cpp: пример реализации паттерна Adapter // #include "pch.h" #include using namespace std; // Уже существующий класс температурного датчика окружающей среды // считаем что этот класс закрыт для изменения // в терминах описания паттерна это Adaptee class FahrenheitSensor { public: // Получить показания температуры в градусах Фаренгейта float getFahrenheitTemp() { float t = 40.0; return t; } }; class Sensor // в терминах описания паттерна Target { public: virtual ~Sensor() {} virtual float getTemperature() = 0; // в терминах описания паттерна request() }; class Adapter : public Sensor // в терминах описаня паттерна Adapter { public: Adapter(FahrenheitSensor* p) : p_fsensor(p) { } ~Adapter() { delete p_fsensor; } 29 float getTemperature() { // в терминах описания паттерна request() return (p_fsensor->getFahrenheitTemp() - 32.0f)*5.0f / 9.0f; } private: FahrenheitSensor* p_fsensor; // в терминах описания паттерна -adaptee }; int main() { Sensor* p = new Adapter(new FahrenheitSensor); cout << "Celsius temperature = " << p->getTemperature() << endl; delete p; } Результаты применения паттерна Adapter Достоинства паттерна Adapter • Паттерн Adapter позволяет повторно использовать уже имеющийся код, адаптируя его несовместимый интерфейс к виду, пригодному для использования. Недостатки паттерна Adapter • Задача преобразования интерфейсов может оказаться непростой в случае, если клиентские вызовы и (или) передаваемые параметры не имеют функционального соответствия в адаптируемом объекте. Мост (Bridge) App41 Паттерн Bridge отделяет абстракцию от реализации так, что то и другое можно изменять независимо. Назначение паттерна Bridge В системе могут существовать классы, отношения между которыми строятся в соответствии со следующей объектно-ориентированной иерархией: абстрактный базовый класс объявляет интерфейс, а конкретные подклассы реализуют его нужным образом. Такой подход является стандартным в ООП, однако, ему свойственны следующие недостатки: 1. Система, построенная на основе наследования, является статичной. Реализация жестко привязана к интерфейсу. Изменить реализацию объекта некоторого типа в процессе выполнения программы уже невозможно. 2. Система становится трудно поддерживаемой, если число родственных производных классов становится большим. Поясним сложности расширения системы новыми типами на примере разработки логгера. Логгер это система протоколирования сообщений, позволяющая фиксировать ошибки, отладочную и другую информацию в процессе выполнения программы. Разрабатываемый нами логгер может использоваться в одном из трех режимов: выводить сообщения на экран, 30 в файл или отсылать их на удаленный компьютер. Кроме того, необходимо обеспечить возможность его применения в одно- и многопоточной средах. Стандартный подход на основе полиморфизма использует следующую иерархию классов. Видно, что число родственных подклассов в системе равно 6. Добавление еще одного вида логгера увеличит его до 8, двух - до 10 и так далее. Система становится трудно управляемой. Указанные недостатки отсутствует в системе, спроектированной с применением паттерна Bridge. Описание паттерна Bridge Паттерн Bridge разделяет абстракцию и реализацию на две отдельные иерархии классов так, что их можно изменять независимо друг от друга. Первая иерархия определяет интерфейс абстракции, доступный пользователю. Для случая проектируемого нами логгера абстрактный базовый класс Logger мог бы объявить интерфейс метода log() для вывода сообщений. Класс Logger также содержит указатель на реализацию pimpl, который инициализируется должным образом при создании логгера конкретного типа. 31 Этот указатель используется для перенаправления пользовательских запросов в реализацию. Заметим, в общем случае подклассы ConsoleLogger, FileLogger и SocketLogger могут расширять интерфейс класса Logger. Все детали реализации, связанные с особенностями среды скрываются во второй иерархии. Базовый класс LoggerImpl объявляет интерфейс операций, предназначенных для отправки сообщений на экран, файл и удаленный компьютер, а подклассы ST_LoggerImpl и МT_LoggerImpl его реализуют для однопоточной и многопоточной среды соответственно. В общем случае, интерфейс LoggerImpl необязательно должен в точности соответствовать интерфейсу абстракции. Часто он выглядит как набор низкоуровневых примитивов. Паттерн Bridge позволяет легко изменить реализацию во время выполнения программы. Для этого достаточно перенастроить указатель pimpl на объект-реализацию нужного типа. Применение паттерна Bridge также позволяет сократить общее число подклассов в системе, что делает ее более простой в поддержке. Структура паттерна Bridge UML-диаграмма классов паттерна Bridge Пример реализации App41 // Logger.h - Абстракция #pragma once #include using namespace std; // Опережающее объявление class LoggerImpl; 32 class Logger { public: Logger(LoggerImpl* p); virtual ~Logger(); virtual void log(string & str) = 0; protected: LoggerImpl * pimpl; }; class ConsoleLogger : public Logger { public: ConsoleLogger(); void log(string & str); }; class FileLogger : public Logger { public: FileLogger(string & file_name); void log(string & str); private: string file; }; class SocketLogger : public Logger { public: SocketLogger(string & remote_host, int remote_port); void log(string & str); private: string host; int port; }; // Logger.cpp - Абстракция #include "pch.h" #include "Logger.h" #include "LoggerImpl.h" using namespace std; Logger::Logger(LoggerImpl* p) : pimpl(p) { } Logger::~Logger() { delete pimpl; } ConsoleLogger::ConsoleLogger() : Logger( #ifdef MT new MT_LoggerImpl() #else new ST_LoggerImpl() #endif ) { } void ConsoleLogger::log(string & str) { pimpl->console_log(str); } 33 FileLogger::FileLogger(string & file_name) : Logger( #ifdef MT new MT_LoggerImpl() #else new ST_LoggerImpl() #endif ), file(file_name) { } void FileLogger::log(string & str) { pimpl->file_log(file, str); } SocketLogger::SocketLogger(string & remote_host, int remote_port) : Logger( #ifdef MT new MT_LoggerImpl() #else new ST_LoggerImpl() #endif ), host(remote_host), port(remote_port) { } void SocketLogger::log(string & str) { pimpl->socket_log(host, port, str); } #pragma once // LoggerImpl.h - Реализация #include using namespace std; class LoggerImpl { public: virtual ~LoggerImpl() {} virtual void console_log(string & str) = 0; virtual void file_log( string & file, string & str) = 0; virtual void socket_log( string & host, int port, string & str) = 0; }; class ST_LoggerImpl : public LoggerImpl { public: void console_log(string & str); void file_log(string & file, string & str); void socket_log( string & host, int port, string & str); }; class MT_LoggerImpl : public LoggerImpl { public: void console_log(string & str); void file_log(string & file, string & str); void socket_log( string & host, int port, string & str); }; 34 // LoggerImpl.cpp - Реализация #include "pch.h" #include #include "LoggerImpl.h" void ST_LoggerImpl::console_log(string & str) { cout << "Single-threaded console logger:" << str << endl; } void ST_LoggerImpl::file_log(string & file, string & str) { cout << "Single-threaded file logger:" << str << endl; } void ST_LoggerImpl::socket_log( string & host, int port, string & str) { cout << "Single-threaded socket logger:" << str << endl; }; void MT_LoggerImpl::console_log(string & str) { cout << "Multithreaded console logger:" << str << endl; } void MT_LoggerImpl::file_log(string & file, string & str) { cout << "Multithreaded file logger:" << str << endl; } void MT_LoggerImpl::socket_log( string & host, int port, string & str) { cout << "Multithreaded socket logger:" << str << endl; } #include #include #include #include "pch.h" "Logger.h" int main() { setlocale(LC_ALL, "rus"); string s = "log.txt"; string text = "Текст передаваемого сообщения"; Logger * p = new FileLogger(s); p->log(text); delete p; Logger *c = new ConsoleLogger(); c->log(text); delete c; return 0; } 35 Паттерны Bridge и Adapter имеют схожую структуру, однако, цели их использования различны. Если паттерн Adapter применяют для адаптации уже существующих классов в систему, то паттерн Bridge используется на стадии ее проектирования. Отметим несколько важных моментов приведенной реализации паттерна Bridge: 1. При модификации реализации клиентский код перекомпилировать не нужно. Использование в абстракции указателя на реализацию (идиома pimpl) позволяет заменить в файле Logger.h включение include "LoggerImpl.h" на опережающее объявление class LoggerImpl. Такой прием снимает зависимость времени компиляции файла Logger.h (и, соответственно, использующих его файлов клиента) от файла LoggerImpl.h. 2. Пользователь класса Logger не видит никаких деталей его реализации. Результаты применения паттерна Bridge Достоинства паттерна Bridge • Проще расширять систему новыми типами за счет сокращения общего числа родственных подклассов. • Возможность динамического изменения реализации в процессе выполнения программы. • Паттерн Bridge полностью скрывает реализацию от клиента. В случае модификации реализации пользовательский код не требует перекомпиляции. Компоновщик (Composite) Паттерн Composite группирует схожие объекты в древовидные структуры. Рассматривает единообразно простые и сложные объекты. Назначение паттерна Composite Используйте паттерн Composite если: • Необходимо объединять группы схожих объектов и управлять ими. • Объекты могут быть как примитивными (элементарными), так и составными (сложными). Составной объект может включать в себя коллекции других объектов, образуя сложные древовидные структуры. Пример: директория файловой системы состоит из элементов, каждый их которых также может быть директорией. • Код клиента работает с примитивными и составными объектами единообразно. Описание паттерна Composite Управление группами объектов может быть непростой задачей, особенно, если эти объекты содержат собственные объекты. Для военной стратегической игры ”Пунические войны ”, описывающей военное противостояние между Римом и Карфагеном каждая боевая единица (всадник, лучник, пехотинец) имеет свою собственную разрушающую силу. Эти единицы могут объединяться в группы для образования более сложных военных подразделений, например, римские 36 легионы, которые, в свою очередь, объединяясь, образуют целую армию. Как рассчитать боевую мощь таких иерархических соединений? Паттерн Composite предлагает следующее решение. Он вводит абстрактный базовый класс Component с поведением, общим для всех примитивных и составных объектов. Для случая стратегической игры - это метод getStrength() для подсчета разрушающей силы. Подклассы Primitive и Composite являются производными от класса Component. Составной объект Composite хранит компоненты-потомки абстрактного типа Component, каждый из которых может быть также Composite. UML-диаграмма классов паттерна Composite Для добавления или удаления объектов-потомков в составной объект Composite, класс Component определяет интерфейсы add() и remove(). 37 // App8_Composite_pattern.cpp : Этот файл содержит функцию "main". // Здесь начинается и заканчивается выполнение программы. // #include #include #include #include "pch.h" using namespace std; class Unit { public: virtual int getStrength() = 0; virtual void addUnit(Unit* p) { assert(false); } virtual ~Unit() {} }; // Primitives class Archer : public Unit { public: virtual int getStrength() { return 1; } }; class Infantryman : public Unit { public: virtual int getStrength() { return 2; } }; class Horseman : public Unit { public: virtual int getStrength() { return 3; } }; class CompositeUnit : public Unit { public: int getStrength() { int total = 0; for (int i = 0; i < c.size(); ++i) total += c[i]->getStrength(); return total; } void addUnit(Unit* p) { c.push_back(p); } 38 ~CompositeUnit() { for (int i = 0; i < c.size(); ++i) delete c[i]; } private: std::vector c; }; // Вспомогательная функция для создания легиона CompositeUnit* createLegion() { // Римский легион содержит: CompositeUnit* legion = new CompositeUnit; // 3000 тяжелых пехотинцев for (int i = 0; i < 3000; ++i) legion->addUnit(new Infantryman); // 1200 легких пехотинцев for (int i = 0; i < 1200; ++i) legion->addUnit(new Archer); // 300 всадников for (int i = 0; i < 300; ++i) legion->addUnit(new Horseman); return legion; } int main() { setlocale(LC_ALL, "rus"); // Римская армия состоит из 4-х легионов CompositeUnit* army = new CompositeUnit; for (int i = 0; i < 4; ++i) army->addUnit(createLegion()); cout << "Римская армия: " << army->getStrength() << endl; // … delete army; return 0; } Результаты применения паттерна Composite Достоинства паттерна Composite • В систему легко добавлять новые примитивные или составные объекты, так как паттерн Composite использует общий базовый класс Component. • Код клиента имеет простую структуру – примитивные и составные объекты обрабатываются одинаковым образом. • Паттерн Composite позволяет легко обойти все узлы древовидной структуры Недостатки паттерна Composite • Неудобно осуществить запрет на добавление в составной объект Composite объектов определенных типов. Так, например, в состав римской армии не могут входить боевые слоны. 39 Паттерн Декоратор (Decorator) Паттерн Decorator используется для расширения функциональности объектов. Являясь гибкой альтернативой порождению классов, паттерн Decorator динамически добавляет объекту новые обязанности. Назначение паттерна Decorator • • • Паттерн Decorator динамически добавляет новые обязанности объекту. Декораторы являются гибкой альтернативой порождению подклассов для расширения функциональности. Рекурсивно декорирует основной объект. Паттерн Decorator использует схему "обертываем подарок, кладем его в коробку, обертываем коробку". Решаемая проблема Вы хотите добавить новые обязанности в поведении или состоянии отдельных объектов во время выполнения программы. Использование наследования не представляется возможным, поскольку это решение статическое и распространяется целиком на весь класс. UML-диаграмма классов паттерна Decorator Пример реализации паттерна Декоратор 40 // APP9_Decorator_pattern.cpp : Этот файл содержит функцию "main". Здесь начинается и заканчивается выполнение программы. // #include "pch.h" #include using namespace std; // НОЗ (LCD) class I { public: virtual ~I() {} virtual void do_it() = 0; }; // Основной объект с базовой функциональностью class A : public I { public: ~A() { cout << "A dtor" << '\n'; } /*virtual*/ void do_it() { cout << 'A'; } }; // Decorator class D : public I { public: D(I *inner) { 41 m_wrappee = inner; } ~D() { delete m_wrappee; } /*virtual*/ void do_it() { m_wrappee->do_it(); } private: I *m_wrappee; // указатель на LCD }; // доп.функциональность class X : public D { public: X(I *core) : D(core) {} ~X() { cout << "X dtor" << " } /*virtual*/ void do_it() { D::do_it(); cout << 'X'; } }; // доп.функциональность class Y : public D { public: Y(I *core) : D(core) {} ~Y() { cout << "Y dtor" << " } /*virtual*/ void do_it() { D::do_it(); cout << 'Y'; } }; // доп.функциональность class Z : public D { public: Z(I *core) : D(core) {} ~Z() { cout << "Z dtor" << " } /*virtual*/ void do_it() { D::do_it(); cout << 'Z'; } }; "; "; "; 42 int main() { I *anX = new X(new A); I *anXY = new Y(new X(new A)); I *anXYZ = new Z(new Y(new X(new A))); I *anZYX = new X(new Y(new Z(new A))); anX->do_it(); cout << '\n'; anXY->do_it(); cout << '\n'; anXYZ->do_it(); cout << '\n'; anZYX->do_it(); cout << '\n'; delete anX; delete anXY; delete anXYZ; delete anZYX; } Использование паттерна Decorator • Подготовьте исходные данные: один основной компонент и несколько дополнительных (необязательных) "оберток". • Создайте общий для всех классов интерфейс по принципу "наименьшего общего знаменателя НОЗ" (lowest common denominator LCD). Этот интерфейс должен делать все классы взаимозаменяемыми. • Создайте базовый класс второго уровня (Decorator) для поддержки дополнительных декорирующих классов. • Основной класс и класс Decorator наследуют общий НОЗ-интерфейс. • Класс Decorator использует отношение композиции. Указатель на НОЗ-объект инициализируется в конструкторе. • Класс Decorator делегирует выполнение операции НОЗ-объекту. • Для реализации каждой дополнительной функциональности создайте класс, производный от Decorator. • Подкласс Decorator реализует дополнительную функциональность и делегирует выполнение операции базовому классу Decorator. • Клиент несет ответственность за конфигурирование системы: устанавливает типы и последовательность использования основного объекта и декораторов. Фасад (Facade) 43 Паттерн Facade предоставляет высокоуровневый унифицированный интерфейс к набору интерфейсов некоторой подсистемы, что облегчает ее использование. Назначение паттерна Facade • Паттерн Facade предоставляет унифицированный интерфейс вместо набора интерфейсов некоторой подсистемы. Facade определяет интерфейс более высокого уровня, упрощающий использование подсистемы. • Паттерн Facade "обертывает" сложную подсистему более простым интерфейсом. Решаемая проблема Клиенты хотят получить упрощенный интерфейс к общей функциональности сложной подсистемы. Обсуждение паттерна Facade Паттерн Facade инкапсулирует сложную подсистему в единственный интерфейсный объект. Это позволяет сократить время изучения подсистемы, а также способствует уменьшению степени связанности между подсистемой и потенциально большим количеством клиентов. С другой стороны, если фасад является единственной точкой доступа к подсистеме, то он будет ограничивать возможности, которые могут понадобиться "продвинутым" пользователям. Объект Facade, реализующий функции посредника, должен оставаться довольно простым и не быть всезнающим "оракулом". Структура паттерна Facade Клиенты общаются с подсистемой через Facade. При получении запроса от клиента объект Facade переадресует его нужному компоненту подсистемы. Для клиентов компоненты подсистемы остаются "тайной, покрытой мраком". UML-диаграмма классов паттерна Facade Пример паттерна Facade Паттерн Facade определяет унифицированный высокоуровневый интерфейс к подсистеме, что упрощает ее использование. Покупатели сталкиваются с фасадом при заказе 44 каталожных товаров по телефону. Покупатель звонит в службу поддержки клиентов и перечисляет товары, которые хочет приобрести. Представитель службы выступает в качестве "фасада", обеспечивая интерфейс к отделу исполнения заказов, отделу продаж и службе доставки. // App6_Facade_pattern.cpp : Этот файл содержит функцию "main". Здесь начинается и заканчивается выполнение программы. // #include "pch.h" #include #include using namespace std; // Абстрактный музыкант не является //обязательной составляющей паттерна //введен для упрощения кода class Musician { string name; public: Musician(string &name) { this->name = name; } virtual ~Musician() {} protected: void output(string text) { std::cout << this->name << " " << text << "." << std::endl; } }; /* конкретные музыканты */ class Vocalist : public Musician { public: Vocalist(string name) : Musician(name) {} void singCouplet(const int coupletNumber) { string s = " спел куплет " + to_string(coupletNumber); output(s); 45 } void singChorus() { output(" спел припев "); } }; class Guitarist : public Musician { public: Guitarist(string name) : Musician(name) {} void playCoolOpening() { output(" начинает с крутого вступления "); } void playFinalAccord() { output(" заканчивает песню мощным аккордом "); } }; class Drummer : public Musician { public: Drummer(string name) : Musician(name) {} void startPlaying() { output(" начинает играть "); } void stopPlaying() { output(" заканчивает играть "); } }; /** Фасад - в данном случае рок-группа */ class Band { Vocalist* vocalist; Guitarist* guitarist; Drummer* drummer; public: Band() { vocalist = new Vocalist("Оззи Осборн"); guitarist = new Guitarist("Тони Айомми"); drummer = new Drummer("Билл Уорд"); } ~Band() { delete vocalist; delete guitarist; delete drummer; } void playCoolSong() { guitarist->playCoolOpening(); drummer->startPlaying(); vocalist->singCouplet(1); 46 vocalist->singChorus(); vocalist->singCouplet(2); vocalist->singChorus(); vocalist->singCouplet(3); vocalist->singChorus(); drummer->stopPlaying(); guitarist->playFinalAccord(); } }; int main() { setlocale(LC_ALL, "rus"); Band* band = new Band(); band->playCoolSong(); return 0; } Использование паттерна Facade • Определите для подсистемы простой, унифицированный интерфейс. • Спроектируйте класс "обертку", инкапсулирующий подсистему. • Вся сложность подсистемы и взаимодействие ее компонентов скрыты от клиентов. "Фасад" / "обертка" переадресует пользовательские запросы подходящим методам подсистемы. • Клиент использует только "фасад". • Рассмотрите вопрос о целесообразности создания дополнительных "фасадов". Особенности паттерна Facade • Facade определяет новый интерфейс, в то время как Adapter использует уже имеющийся. Помните, Adapter делает работающими вместе два существующих интерфейса, не создавая новых. • Если Flyweight показывает, как сделать множество небольших объектов, то Facade показывает, как сделать один объект, представляющий целую подсистему. • Mediator похож на Facade тем, что абстрагирует функциональность существующих классов. Однако Mediator централизует функциональность между объектамиколлегами, не присущую ни одному из них. Коллеги обмениваются информацией друг с другом через Mediator. С другой стороны, Facade определяет простой интерфейс к подсистеме, не добавляет новой функциональности и не известен классам подсистемы. • Abstract Factory может применяться как альтернатива Facade для сокрытия платформенно-зависимых классов. 47 Объекты "фасадов" часто являются Singleton, потому что требуется только один объект Facade. • Adapter и Facade в являются "обертками", однако эти "обертки" разных типов. Цель Facade – создание более простого интерфейса, цель Adapter – адаптация существующего интерфейса. Facade обычно "обертывает" несколько объектов, Adapter "обертывает" один объект. • http://cpp-reference.ru/patterns/structural-patterns/ Паттерн «Приспособленец» (FlyWeight) Назначение паттерна Flyweight • Паттерн Flyweight использует разделение для эффективной поддержки большого числа мелких объектов. Решаемая проблема Проектирование системы из объектов самого низкого уровня обеспечивает оптимальную гибкость, но может быть неприемлемо "дорогим" решением с точки зрения производительности и расхода памяти. Обсуждение паттерна Flyweight Паттерн Flyweight описывает, как совместно разделять очень мелкие объекты без чрезмерно высоких издержек. Каждый объект-приспособленец имеет две части: внутреннее и внешнее состояния. Внутреннее состояние хранится (разделяется) в приспособленце и состоит из информации, не зависящей от его контекста. Внешнее состояние хранится или вычисляется объектами-клиентами и передается приспособленцу при вызове его методов. Структура паттерна Flyweight Клиенты не создают приспособленцев напрямую, а запрашивают их у фабрики. Любые атрибуты (данные-члены класса), которые не могут разделяться, являются внешним состоянием. Внешнее состояние передается приспособленцу при вызове его методов. При этом наибольшая экономия памяти достигается в том случае, если внешнее состояние не хранится, а вычисляется при вызове. 48 Пример паттерна Flyweight Паттерн Flyweight использует разделение для эффективной поддержки большого числа мелких объектов. Телефонная сеть общего пользования ТФОП является примером Flyweight. Такие ресурсы как генераторы тональных сигналов), приемники цифр номера абонента, набираемого в тоновом наборе, являются общими для всех абонентов. Когда абонент поднимает трубку, чтобы позвонить, ему предоставляется доступ ко всем нужным разделяемым ресурсам. Использование паттерна Flyweight • Убедитесь, что существует проблема повышенных накладных расходов. • Разделите состояние целевого класса на разделяемое (внутреннее) и неразделяемое (внешнее). 49 • Удалите из атрибутов (членов данных) класса неразделяемое состояние и добавьте его в список аргументов, передаваемых методам. • Создайте фабрику, которая может кэшировать и повторно использовать существующие экземпляры класса. • Для создания новых объектов клиент использует эту фабрику вместо оператора new. • Клиент (или третья сторона) должен находить или вычислять неразделяемое состояние и передавать его методам класса. Особенности паттерна Flyweight Если Flyweight показывает, как сделать множество небольших объектов, то Facade показывает, как представить целую подсистему одним объектом. • Flyweight часто используется совместно с Composite для реализации иерархической структуры в виде графа с разделяемыми листовыми вершинами. • Терминальные символы абстрактного синтаксического дерева Interpreter могут разделяться при помощи Flyweight. • Flyweight объясняет, когда и как могут разделяться объекты State. • Реализация Flyweight – 50 // App5_Flyweight_Pattern.cpp : Этот файл содержит функцию "main". // Здесь начинается и заканчивается выполнение программы. // // пример реализации паттерна Flyweight (приспособленец) #include "pch.h" #include #include using namespace std; class Icon { public: Icon(string fileName) // приспособленец { //strcpy_s(_name, sizeof(_name), fileName); _name = fileName; if (fileName=="go") //(!strcmp(fileName, "go")) { _width = 20; _height = 20; } if (fileName=="stop") //(!strcmp(fileName, "stop")) { _width = 40; _height = 40; } if (fileName=="select") //(!strcmp(fileName, "select")) { _width = 60; _height = 60; } if (fileName=="undo") //(!strcmp(fileName, "undo")) { _width = 30; _height = 30; } } const string getName() { return _name; } void draw(int x, int y) 51 { cout << " drawing " << _name << ": upper left (" << x << "," << y << ") - lower right (" << x + _width << "," << y + _height << ")" << endl; } private: string _name; // char _name[20]; int _width; int _height; }; class FlyweightFactory { public: static Icon *getIcon(string str) { //char *name = new char[str.length() + 1]; //strcpy(name, str.c_str()); //for (int i = 0; i < str.length(); i++) // name[i] = (char)str[i]; for (int i = 0; i < _numIcons; i++) if (str== _icons[i]->getName()) //(!strcmp(name, _icons[i]->getName())) return _icons[i]; _icons[_numIcons] = new Icon(str); return _icons[_numIcons++]; } static void reportTheIcons() { cout << "Active Flyweights: "; for (int i = 0; i < _numIcons; i++) cout << _icons[i]->getName() << " "; cout << endl; } private: enum { MAX_ICONS = 5 }; static int _numIcons; static Icon *_icons[MAX_ICONS]; }; int FlyweightFactory::_numIcons = 0; Icon *FlyweightFactory::_icons[]; class DialogBox { public: DialogBox(int x, int y, int incr) : _iconsOriginX(x), _iconsOriginY(y), _iconsXIncrement(incr) {} virtual void draw() = 0; protected: Icon *_icons[3]; int _iconsOriginX; int _iconsOriginY; int _iconsXIncrement; }; class FileSelection : public DialogBox { public: FileSelection(Icon *first, Icon *second, Icon *third) : DialogBox(100, 100, 100) { _icons[0] = first; _icons[1] = second; _icons[2] = third; } void draw() { 52 cout << "drawing FileSelection:" << endl; for (int i = 0; i < 3; i++) _icons[i]->draw(_iconsOriginX + (i *_iconsXIncrement), _iconsOriginY); } }; class CommitTransaction : public DialogBox { public: CommitTransaction(Icon *first, Icon *second, Icon *third) : DialogBox(150, 150, 150) { _icons[0] = first; _icons[1] = second; _icons[2] = third; } void draw() { cout << "drawing CommitTransaction:" << endl; for (int i = 0; i < 3; i++) _icons[i]->draw(_iconsOriginX + (i *_iconsXIncrement), _iconsOriginY); } }; int main() { DialogBox *dialogs[2]; dialogs[0] = new FileSelection( FlyweightFactory::getIcon("go"), FlyweightFactory::getIcon("stop"), FlyweightFactory::getIcon("select")); dialogs[1] = new CommitTransaction( FlyweightFactory::getIcon("select"), FlyweightFactory::getIcon("stop"), FlyweightFactory::getIcon("undo")); for (int i = 0; i < 2; i++) dialogs[i]->draw(); FlyweightFactory::reportTheIcons(); } Паттерн Flyweight показывает, как эффективно разделять множество мелких объектов. Ключевая концепция - различие между внутренним и внешним состояниями. Внутреннее состояние состоит из информации, которая не зависит от контекста и может разделяться (например, имя иконки, ее ширина и высота). Оно хранится в приспособленце (то есть в классе Icon). 53 Внешнее состояние не может разделяться, оно зависит от контекста и изменяется вместе с ним (например, координаты верхнего левого угла для каждого экземпляра иконки). Внешнее состояние хранится или вычисляется клиентом и передается приспособленцу при вызове операций. Клиенты не должны создавать экземпляры приспособленцев напрямую, а получать их исключительно из объекта FlyweightFactory для правильного разделения. «Прокси» (Proxy) (заместитель, surrogate, суррогат) Паттерн Proxy замещает другой объект для контроля доступа к нему. Назначение паттерна Proxy • Паттерн Proxy является суррогатом или замеcтителем другого объекта и контролирует доступ к нему. • Предоставляя дополнительный уровень косвенности при доступе к объекту, может применяться для поддержки распределенного, управляемого или интеллектуального доступа. • Являясь "оберткой" реального компонента, защищает его от излишней сложности. Решаемая проблема Вам нужно управлять ресурсоемкими объектами. Вы не хотите создавать экземпляры таких объектов до момента их реального использования. Обсуждение паттерна Proxy Суррогат или заместитель это объект, интерфейс которого идентичен интерфейсу реального объекта. При первом запросе клиента заместитель создает реальный объект, сохраняет его адрес и затем отправляет запрос этому реальному объекту. Все последующие запросы просто переадресуются инкапсулированному реальному объекту. Существует четыре ситуации, когда можно использовать паттерн Proxy: 54 • Виртуальный proxy является заместителем объектов, создание которых обходится дорого. Реальный объект создается только при первом запросе/доступе клиента к объекту. • Удаленный proxy предоставляет локального представителя для объекта, который находится в другом адресном пространстве ("заглушки" в RPC и CORBA). • Защитный proxy контролирует доступ к основному объекту. "Суррогатный" объект предоставляет доступ к реальному объекту, только вызывающий объект имеет соответствующие права. • Интеллектуальный proxy выполняет дополнительные действия при доступе к объекту. Вот типичные области применения интеллектуальных proxy: • Подсчет числа ссылок на реальный объект. При отсутствии ссылок память под объект автоматически освобождается (известен также как интеллектуальный указатель или smart pointer). • Загрузка объекта в память при первом обращении к нему. • Установка запрета на изменение реального объекта при обращении к нему других объектов. Структура паттерна Proxy Заместитель Proxy и реальный объект RealSubject имеют одинаковые интерфейсы класса Subject, поэтому заместитель может использоваться "прозрачно" для клиента вместо реального объекта. UML – диаграмма Proxy 55 Пример паттерна Proxy // App7_Proxy_pattern.cpp : Этот файл содержит функцию "main". // Здесь начинается и заканчивается выполнение программы. // Пример реализации паттерна Proxy #include "pch.h" #include using namespace std; class RealImage { int m_id; public: RealImage(int i) { m_id = i; cout << "RealImage конструктор: " << m_id << endl; } ~RealImage() { cout << "RealImage деструктор: " << m_id << endl; } void draw() { cout << " RealImage::draw() " << m_id << endl; } }; // 1. Класс-обертка с "дополнительным уровнем косвенности" class Image { // 2. Класс-обертка содержит закрытый указатель на реальный класс RealImage *ptrRealImage; int m_id; static int s_next; public: Image() { m_id = s_next++; // 3. Инициализируется нулевым значением ptrRealImage = nullptr; } 56 ~Image() { delete ptrRealImage; } void draw() { // 4. Реальный объект создается при поступлении // запроса "на первом использовании" if (!ptrRealImage) ptrRealImage = new RealImage(m_id); // 5. Запрос всегда делегируется реальному объекту ptrRealImage->draw(); } }; int Image::s_next = 1; int main() { setlocale(LC_ALL, "rus"); Image images[5]; for (int i; true;) { cout << "Для выхода введите 0, для работы 1..5: "; cin >> i; if (i == 0) break; images[i - 1].draw(); } } Паттерн Proxy для доступа к реальному объекту использует его суррогат или заместитель. Банковский чек является заместителем денежных средств на счете. Чек может быть использован вместо наличных денег для совершения покупок и, в конечном счете, контролирует доступ к наличным деньгам на счете чекодателя. Использование паттерна Proxy Определите ту часть системы, которая лучше всего реализуется через суррогата. • Определите интерфейс, который сделает суррогата и оригинальный компонент взаимозаменяемыми. • Рассмотрите вопрос об использовании фабрики, инкапсулирующей решение о том, что желательно использовать на практике: оригинальный объект или его суррогат. • Класс суррогата содержит указатель на реальный объект и реализует общий интерфейс. • 57 Указатель на реальный объект может инициализироваться в конструкторе или при первом использовании. • Методы суррогата выполняют дополнительные действия и вызывают методы реального объекта. • Особенности паттерна Proxy • Adapter предоставляет своему объекту другой интерфейс . Proxy предоставляет тот же интерфейс. Decorator предоставляет расширенный интерфейс. • Decorator и Proxy имеют разные цели, но схожие структуры. Оба вводят дополнительный уровень косвенности: их реализации хранят ссылку на объект, на который они отправляют запросы. 58 ЛЕКЦИЯ 11 Тема: Типовые решения для объекто-ориентированных программ Шаблоны (pattern) проектирования. Поведенческие шаблоны. Паттерны поведения определяют алгоритмы и способы реализации взаимодействия различных объектов и классов. Они обеспечивают гибкость взаимодействия между объектами. Паттерны поведения Chain с Responsibility of Цепочка обязанностей 2 Command Команда 3 Interpreter Интерпретатор 4 Iterator Итератор 5 Mediator Посредник 6 Memento Хранитель 7 Observer Наблюдатель 8 State Состояние 9 Strategy Стратегия 10 Template Method Шаблонный метод 11 Visitor Посетитель Позволяет избежать жесткой зависимости отправителя запроса от его получателя, при этом объекты-получатели связываются в цепочку, а запрос передается по цепочке, пока какой-то объект его не обработает. Инкапсулирует запрос в виде объекта, обеспечивая параметризацию клиентов типом запроса, установление очередности запросов, протоколирование запросов и отмену выполнения операций. Для заданного языка определяет представление его грамматики на основе интерпретатора предложений языка, использующего это представление. Дает возможность последовательно перебрать все элементы составного объекта, не раскрывая его внутреннего представления. Определяет объект, в котором инкапсулировано знание о том, как взаимодействуют объекты из некоторого множества. Способствует уменьшению числа связей между объектами, позволяя им работать без явных ссылок друг на друга и независимо изменять схему взаимодействия. Дает возможность получить и сохранить во внешней памяти внутреннее состояние объекта, чтобы позже объект можно было восстановить точно в таком же состоянии, не нарушая принципа инкапсуляции. Специфицирует зависимость типа "один ко многим" между различными объектами, так что при изменении состояния одного объекта все зависящие от него получают извещение и автоматически обновляются. Позволяет выбранному объекту варьировать свое поведение при изменении внутреннего состояния. При этом создается впечатление, что изменился класс объекта. Определяет множество алгоритмов, инкапсулируя их все и позволяя подставлять один вместо другого. При этом можно изменять алгоритм независимо от клиента, который им пользуется. Определяет структуру алгоритма, перераспределяя ответственность за некоторые его шаги на подклассы. При этом подклассы могут переопределять шаги алгоритма, не меняя его общей структуры. Позволяет определить новую операцию, не меняя описаний классов, у объектов которых она вызывается. Паттерн «Команда» (Command) 59 Назначение паттерна Command Паттерн Command используется, если: • Система управляется событиями. При появлении такого события (запроса) необходимо выполнить определенную последовательность действий. • Необходимо параметризировать объекты выполняемым действием, ставить запросы в очередь или поддерживать операции отмены (undo) и повтора (redo) действий. • Нужен объектно-ориентированный аналог функции обратного (callback) вызова в процедурном программировании. Пример событийно-управляемой системы – приложение с пользовательским интерфейсом. При выборе некоторого пункта меню пользователем вырабатывается запрос на выполнение определенного действия (например, открытия файла). Описание паттерна Command Паттерн Command преобразовывает запрос на выполнение действия в отдельный объект-команду. Этот объект запроса на действие и называется командой. При этом объекты, инициирующие запросы на выполнение действия, отделяются от объектов, которые выполняют это действие. Такая инкапсуляция позволяет передавать эти действия другим функциям и объектам в качестве параметра, приказывая им выполнить запрошенную операцию. Команда – это объект, поэтому над ней допустимы любые операции, что и над объектом. Интерфейс командного объекта определяется абстрактным базовым классом Command и в самом простом случае имеет единственный метод execute(). Производные классы определяют получателя запроса (указатель на объект-получатель) и необходимую для выполнения операцию (метод этого объекта). Метод execute() подклассов Command просто вызывает нужную операцию получателя. В паттерне Command может быть до трех участников: • Клиент, создающий экземпляр командного объекта. • Инициатор запроса, использующий командный объект. • Получатель запроса. UML — диаграмма классов паттерна “Command”. Сначала клиент создает объект ConcreteCommand, конфигурируя его получателем запроса. Этот объект также доступен инициатору. Инициатор использует его при отправке запроса, вызывая метод execute(). Этот алгоритм напоминает работу функции обратного вызова в процедурном программировании – функция регистрируется, чтобы быть вызванной позднее. Паттерн Command отделяет объект, инициирующий операцию, от объекта, который знает, как ее выполнить. Единственное, что должен знать инициатор, это как отправить команду. Это придает системе гибкость: позволяет осуществлять динамическую замену команд, использовать сложные составные команды, осуществлять отмену операций. 60 // App10_Command_pattern.cpp : Этот файл содержит функцию "main". 61 // Здесь начинается и заканчивается выполнение программы. // #include "pch.h" #include using namespace std; // получатель команды class Receiver { public: void Operaiton1() { cout << "Receiver::Operation1() " << endl; } void Operaiton2() { cout << "Receiver::Operation2() " << endl; } }; class Command { public: virtual ~Command() {} virtual void Execute() = 0; protected: Command(Receiver *p) : ptrReceiver(p) {} Receiver *ptrReceiver; }; // конкретная команда class ConcreteCommand1 : public Command { public: ConcreteCommand1(Receiver *p) : Command(p) {} void Execute() { ptrReceiver->Operaiton1(); } }; class ConcreteCommand2 : public Command { public: ConcreteCommand2(Receiver *p) : Command(p) {} void Execute() { ptrReceiver->Operaiton2(); } }; // инициатор команды class Invoker { Command *ptrCommand; public: void SetCommand(Command *ptrC) { ptrCommand = ptrC; } void Run() 62 { ptrCommand->Execute(); } }; //class Client int main() { Receiver receiver; Invoker invoker; Command *command1 = new ConcreteCommand1(&receiver); Command *command2 = new ConcreteCommand2(&receiver); invoker.SetCommand(command1); invoker.Run(); invoker.SetCommand(command2); invoker.Run(); delete command1; delete command2; } Достоинства паттерна Command • Придает системе гибкость, отделяя инициатора запроса от его получателя. Паттерн «Итератор» (Iterator) Назначение паттерна Iterator • Предоставляет способ последовательного доступа ко всем элементам составного объекта, не раскрывая его внутреннего представления. • Абстракция в стандартных библиотеках C++ и Java, позволяющая разделить классы коллекций и алгоритмов. • Придает обходу коллекции "объектно-ориентированный статус". • Полиморфный обход. Решаемая проблема Предлагается реализация механизма "абстрактного" обхода различных структур данных так, что могут определяться алгоритмы, способные взаимодействовать со структурами прозрачно. Обсуждение паттерна Iterator Составной объект, такой как список, должен предоставлять способ доступа к его элементам без раскрытия своей внутренней структуры. Более того, иногда нужно перебирать элементы списка различными способами, в зависимости от конкретной задачи. Кроме того, иногда нужно иметь несколько активных обходов одного списка одновременно. Было бы 63 хорошо иметь единый интерфейс для обхода разных типов составных объектов (т.е. полиморфная итерация). Паттерн Iterator позволяет все это делать. Ключевая идея состоит в том, чтобы ответственность за доступ и обход переместить из составного объекта на объект Iterator, который будет определять стандартный протокол обхода. Абстракция Iterator имеет основополагающее значение для технологии, называемой "обобщенное программирование". Эта технология четко разделяет такие понятия как "алгоритм" и "структура данных". Мотивирующие факторы: способствование компонентной разработке, повышение производительности и снижение расходов на управление. Рассмотрим пример. Если вы хотите одновременно поддерживать четыре вида структур данных (массив, бинарное дерево, связанный список и хэш-таблица) и три алгоритма (сортировка, поиск и слияние), то традиционный подход потребует 12 вариантов конфигураций (четыре раза по три), в то время как обобщенное программирование требует лишь 7 (четыре плюс три). Структура паттерна Iterator Для манипулирования коллекцией клиент использует открытый интерфейс класса Collection. Однако доступ к элементам коллекции инкапсулируется дополнительным уровнем абстракции, называемым Iterator. Каждый производный от Collection класс знает, какой производный от Iterator класс нужно создавать и возвращать. После этого клиент использует интерфейс, определенный в базовом классе Iterator. UML — диаграмма классов паттерна “Iterator” 64 Реализация паттерна Iterator Реализация паттерна Iterator с использованием методов Вынесите из коллекции функциональность "обход элементов коллекции" и придайте ей в "объектный статус". Это упростит саму коллекцию, позволит одновременно создавать множество активных обходов и отделит алгоритмы от структур данных коллекции. Каждый контейнерный класс должен иметь итератор. Может показаться, что это является нарушением принципа инкапсуляции, так как пользователи класса Stack получают доступ к его содержимому напрямую. Однако Джон Лакош (John Lakos) приводит следующие аргументы: дизайнер класса неизбежно что-то упустит. Позже, когда пользователям потребуется дополнительная функциональность, если итератор первоначально был предусмотрен, то они смогут добавить эту функциональность в соответствии с принципом "открыт для расширения, закрыт для модификации". Без наличия итератора их единственным выходом было бы докучливое изменение рабочего кода. Ниже исходный класс Stack не содержит оператор равенства, но имеет итератор. В результате, оператор равенства может быть легко добавлен. Использование паттерна Iterator:  Добавьте классу "коллекции" метод create_iterator() и разрешите дружественный доступ классу «итератор».  Спроектируйте класс «итератор» (Iterator), инкапсулирующий обход "коллекции" класса.  Клиенты запрашивают у объекта Collection создание объекта-итератора.  Клиенты используют методы first(), is_done(), next() и current_item() для доступа к элементам класса Collection. // App11_Iterator_pattern.cpp : Этот файл содержит функцию "main". // Здесь начинается и заканчивается выполнение программы. // // Пример реализации паттерна "Итератор" #include "pch.h" #include using namespace std; class Stack { int items[10]; int sp; public: Stack() { sp = -1; } void push(int in) { items[++sp] = in; } int pop() { return items[sp--]; } 65 bool isEmpty() { return (sp == -1); } friend class StackIter; // разрешаю итератору привилигированный доступ // к закрытым членам класса Stack // 2. Добавьте член createIterator() StackIter* createIterator() const; }; // 1. Спроектируйте класс "iterator" class StackIter { const Stack *stk; int index; public: StackIter(const Stack *s) { stk = s; } void first() { index = 0; } void next() { index++; } bool isDone() { return index == stk->sp + 1; } int currentItem() { return stk->items[index]; } }; StackIter * Stack::createIterator()const { return new StackIter(this); } bool operator == (const Stack &l, const Stack &r) { // 3. Клиенты запрашивают создание объекта StackIter у объекта Stack StackIter *itl = l.createIterator(); StackIter *itr = r.createIterator(); // 4. Клиенты используют first(), isDone(), next(), and currentItem() for (itl->first(), itr->first(); !itl->isDone(); itl->next(), itr->next()) if (itl->currentItem() != itr->currentItem()) break; bool ans = itl->isDone() && itr->isDone(); delete itl; delete itr; return ans; } int main() { Stack s1; for (int i = 1; i < 5; i++) 66 s1.push(i); Stack s2(s1), s3(s1), s4(s1), s5(s1); s3.pop(); s5.pop(); s4.push(2); s5.push(9); cout << "s1 == s2 is " << ((s1 == s2) cout << "s1 == s3 is " << ((s1 == s3) cout << "s1 == s4 is " << ((s1 == s4) cout << "s1 == s5 is " << ((s1 == s5) } ? ? ? ? "true" "true" "true" "true" : : : : "false") "false") "false") "false") << << << << endl; endl; endl; endl; Реализация паттерна Iterator: использование операторов вместо методов Джон Лакош отмечает, что интерфейсы GOF-итераторов дают возможность неправильного написания имен методов, являются неуклюжими и требуют слишком много печати. Представленная ниже реализация паттерна Iterator основана на использовании "интуитивных" операторов. Отметим также, что метод createIterator() больше не нужен. Пользователь создает итераторы как локальные переменные, поэтому очистка не требуется. // App12_Inerator_pattern_isp_operator.cpp : // Этот файл содержит функцию "main". Здесь начинается и заканчивается выполнение программы. // // Использование в паттерне Итератор операторов вместо методов #include "pch.h" #include using namespace std; class Stack { int items[10]; int sp; public: friend class StackIter; Stack() { sp = -1; } void push(int in) { items[++sp] = in; } int pop() { return items[sp--]; } bool isEmpty() { return (sp == -1); } }; 67 class StackIter { const Stack &stk; int index; public: StackIter(const Stack &s) : stk(s) { index = 0; } void operator++() { index++; } bool operator()() { return index != stk.sp + 1; } int operator *() { return stk.items[index]; } }; bool operator == (const Stack &l, const Stack &r) { StackIter itl(l), itr(r); for (; itl(); ++itl, ++itr) if (*itl != *itr) break; return !itl() && !itr(); } int main() { Stack s1; int i; for (i = 1; i < 5; i++) s1.push(i); Stack s2(s1), s3(s1), s4(s1), s3.pop(); s5.pop(); s4.push(2); s5.push(9); cout << "s1 == s2 is " << (s1 cout << "s1 == s3 is " << (s1 cout << "s1 == s4 is " << (s1 cout << "s1 == s5 is " << (s1 } s5(s1); == == == == s2) s3) s4) s5) << << << << endl; endl; endl; endl; Особенности паттерна Iterator Iterator может применяться для обхода сложных структур, создаваемых с помощью «компоновщика» Composite.  Для создания экземпляра подкласса Iterator полиморфные итераторы используют «фабричный метод» (Factory Method).  68  Часто «Хранитель» (Memento) и Iterator используются совместно. Iterator может использовать «хранителя» (Memento) для сохранения состояния итерации и содержит его внутри себя. Паттерн «Посредник» Mediator Назначение паттерна Mediator • Паттерн Mediator определяет объект, инкапсулирующий взаимодействие множества объектов. Mediator делает систему слабо связанной, избавляя объекты от необходимости ссылаться друг на друга, что позволяет изменять взаимодействие между ними независимо. • Паттерн Mediator вводит посредника для развязывания множества взаимодействующих объектов. • Заменяет взаимодействие "все со всеми" взаимодействием "один со всеми". Решаемая проблема Мы хотим спроектировать систему с повторно используемыми компонентами, однако существующие связи между этими компонентами можно охарактеризовать феноменом "спагетти-кода". Спагетти-код - плохо спроектированная, слабо структурированная, запутанная и трудная для понимания программа. Спагетти-код назван так, потому что ход выполнения программы похож на миску спагетти, то есть извилистый и запутанный. Обсуждение паттерна Mediator В Unix права доступа к системным ресурсам определяются тремя уровнями: владелец, группа и прочие. Группа представляет собой совокупность пользователей, обладающих некоторой функциональной принадлежностью. Каждый пользователь в системе может быть членом одной или нескольких групп, и каждая группа может иметь 0 или более пользователей, назначенных этой группе. Следующий рисунок показывает трех пользователей, являющихся членами всех трех групп. 69 Если нам нужно было бы построить программную модель такой системы, то мы могли бы связать каждый объект User c каждым объектом Group, а каждый объект Group - с каждым объектом User. Однако из-за наличия множества взаимосвязей модифицировать поведение такой системы очень непросто, пришлось бы изменять все существующие классы. Альтернативный подход - введение "дополнительного уровня косвенности" или построение абстракции из отображения (соответствия) пользователей в группы и групп в пользователей. Такой подход обладает следующими преимуществами: пользователи и группы отделены друг от друга, отображениями легко управлять одновременно и абстракция отображения может быть расширена в будущем путем определения производных классов. Разбиение системы на множество объектов в общем случае повышает степень повторного использования, однако множество взаимосвязей между этими объектами, как правило, приводит к обратному эффекту. Чтобы этого не допустить, инкапсулируйте взаимодействия между объектами в объект-посредник. Действуя как центр связи, этот объект-посредник контролирует и координирует взаимодействие группы объектов. При этом объект-посредник делает взаимодействующие объекты слабо связанными, так как им больше не нужно хранить ссылки друг на друга – все взаимодействие идет через этого посредника. Расширить или изменить это взаимодействие можно через его подклассы. Паттерн Mediator заменяет взаимодействие "все со всеми" взаимодействием "один со всеми". 70 // App13_Mediator_pattern.cpp : Этот файл содержит функцию "main". // Здесь начинается и заканчивается выполнение программы. // #include "pch.h" #include #include class class class class class Colleague; Mediator; ConcreteMediator; ConcreteColleague1; ConcreteColleague2; class Mediator { public: virtual void Send(std::string const& message, Colleague *colleague) const = 0; }; class Colleague { protected: Mediator* mediator_; public: explicit Colleague(Mediator *mediator) :mediator_(mediator) { } }; class ConcreteColleague1 :public Colleague { public: explicit ConcreteColleague1(Mediator* mediator) :Colleague(mediator) {} void Send(std::string const& message) { mediator_->Send(message, this); } void Notify(std::string const& message) { std::cout << "Colleague1 получил сообщение:'" << message << "'" << std::endl; } }; 71 class ConcreteColleague2 :public Colleague { public: explicit ConcreteColleague2(Mediator *mediator) :Colleague(mediator) {} void Send(std::string const& message) { mediator_->Send(message, this); } void Notify(std::string const& message) { std::cout << "Colleague2 получил сообщение:'" << message << "'" << std::endl; } }; class ConcreteMediator :public Mediator { protected: ConcreteColleague1 *m_Colleague1; ConcreteColleague2 *m_Colleague2; public: void SetColleague1(ConcreteColleague1 *c) { m_Colleague1 = c; } void SetColleague2(ConcreteColleague2 *c) { m_Colleague2 = c; } virtual void Send(std::string const& message, Colleague *colleague) const { if (colleague == m_Colleague1) m_Colleague2->Notify(message); else if (colleague == m_Colleague2) m_Colleague1->Notify(message); } }; int main() { setlocale(LC_ALL, "rus"); ConcreteMediator m; ConcreteColleague1 c1(&m); ConcreteColleague2 c2(&m); m.SetColleague1(&c1); m.SetColleague2(&c2); c1.Send("Привет! Как дела?"); c2.Send("Привет! Всё хорошо!"); return 0; } Использование паттерна Mediator • Определите совокупность взаимодействующих объектов, связанность между которыми нужно уменьшить. 72 • • • • Инкапсулируйте все взаимодействия в абстракцию нового класса. Создайте экземпляр этого нового класса. Объекты-коллеги для взаимодействия друг с другом используют только этот объект. Найдите правильный баланс между принципом слабой связанности и принципом распределения ответственности. Будьте внимательны и не создавайте объект-"контроллер" вместо объекта-посредника. Особенности паттерна Mediator Паттерны “Цепочка обязанностей» (Chain of Responsibility), «Команда» (Command), «Посредник» (Mediator) и «Наблюдатель» (Observer) показывают, как можно разделить отправителей и получателей запросов с учетом их особенностей. Chain of Responsibility передает запрос отправителя по цепочке потенциальных получателей. Command номинально определяет связь - "оправитель-получатель" с помощью подкласса. В Mediator отправитель и получатель ссылаются друг на друга косвенно, через объект-посредник. В паттерне Observer связь между отправителем и получателем слабее, при этом число получателей может конфигурироваться во время выполнения. • Mediator и Observer являются конкурирующими паттернами. Если Observer распределяет взаимодействие c помощью объектов "наблюдатель" и "субъект", то Mediator использует объект-посредник для инкапсуляции взаимодействия между другими объектами. Легче сделать повторно используемыми Наблюдателей и Субъектов, чем Посредников. • С другой стороны, Mediator может использовать Observer для динамической регистрации коллег и их взаимодействия с посредником. • Mediator похож Faсade в том, что он абстрагирует функциональность существующих классов. Mediator абстрагирует/централизует взаимодействие между объектамиколлегами, добавляет новую функциональность и известен всем объектам-коллегам (то есть определяет двунаправленный протокол взаимодействия). Facade, наоборот, определяет более простой интерфейс к подсистеме, не добавляя новой функциональности, и неизвестен классам подсистемы (то есть имеет однонаправленный протокол взаимодействия, то есть запросы отправляются в подсистему, но не наоборот). • Паттерн “Хранитель» (Memento) Назначение паттерна Memento • Не нарушая инкапсуляции, паттерн Memento получает и сохраняет за пределами объекта его внутреннее состояние так, чтобы позже можно было восстановить объект в таком же состоянии. • Является средством для инкапсуляции "контрольных точек" программы. • Паттерн Memento придает операциям "Отмена" (undo) или "Откат" (rollback) статус "полноценного объекта". 73 Решаемая проблема Вам нужно восстановить объект обратно в прежнее состояние (те есть выполнить операции "Отмена" или "Откат"). Обсуждение паттерна Memento Клиент запрашивает Memento (хранителя) у исходного объекта, когда ему необходимо сохранить состояние исходного объекта (установить контрольную точку). Исходный объект инициализирует Memento своим текущим состоянием. Клиент является "посыльным" за Memento, но только исходный объект может сохранять и извлекать информацию из Memento (Memento является "непрозрачным" для клиентов и других объектов). Если клиенту в дальнейшем нужно "откатить" состояние исходного объекта, он передает Memento обратно в исходный объект для его восстановления. Реализовать возможность выполнения неограниченного числа операций "Отмена" (undo) и "Повтор" (redo) можно с помощью стека объектов Command и стека объектов Memento. Паттерн проектирования Memento определяет трех различных участников: Originator (хозяин) - объект, умеющий создавать хранителя, а также знающий, как восстановить свое внутреннее состояние из хранителя. • Caretaker (смотритель) - объект, который знает, почему и когда хозяин должен сохранять и восстанавливать себя. • Memento (хранитель) - "ящик на замке", который пишется и читается хозяином и за которым присматривает смотритель. • Паттерн Memento фиксирует и сохраняет за пределами объекта его внутреннее состояние так, чтобы позже этот объект можно было бы восстановить в таком же состоянии. Использование паттерна Memento • • • • • Определите роли "смотрителя" и "хозяина". Создайте класс Memento и объявите хозяина другом. Смотритель знает, когда создавать "контрольную точку" хозяина. Хозяин создает хранителя Memento и копирует свое состояние в этот Memento. Смотритель сохраняет хранителя Memento (но смотритель не может заглянуть в Memento). • Смотритель знает, когда нужно "откатить" хозяина. • Хозяин восстанавливает себя, используя сохраненное в Memento состояние. 74 // App14_Memento_pattern.cpp : Этот файл содержит функцию "main". // Здесь начинается и заканчивается выполнение программы. // реализация паттерна Memento #include "pch.h" #include using namespace std; class Number; // опережающее объявление класса class Memento { public: Memento(int val) { _state = val; } private: friend class Number; int _state; }; class Number { public: Number(int value) { _value = value; } void dubble() { _value = 2 * _value; } void half() { _value = _value / 2; } int getValue() { return _value; } Memento *createMemento() { return new Memento(_value); } void reinstateMemento(Memento *mem) { _value = mem->_state; } private: int _value; }; class Command { public: typedef void(Number:: *Action)(); Command(Number *receiver, Action action) { _receiver = receiver; _action = action; } virtual void execute() { _mementoList[_numCommands] = _receiver->createMemento(); 75 _commandList[_numCommands] = this; if (_numCommands > _highWater) _highWater = _numCommands; _numCommands++; (_receiver->*_action)(); } static void undo() { if (_numCommands == 0) { cout << "*** UNDO Все сделано! ***" << endl; return; } _commandList[_numCommands - 1]->_receiver>reinstateMemento(_mementoList[_numCommands - 1]); _numCommands--; } void static redo() { if (_numCommands > _highWater) { cout << "*** REDO Все сделано! ***" << endl; return; } (_commandList[_numCommands]->_receiver->*(_commandList[_numCommands]>_action))(); _numCommands++; } protected: Number *_receiver; Action _action; static Command *_commandList[20]; static Memento *_mementoList[20]; static int _numCommands; static int _highWater; }; Command *Command::_commandList[]; Memento *Command::_mementoList[]; int Command::_numCommands = 0; int Command::_highWater = 0; int main() { int i; setlocale(LC_ALL, "rus"); cout << "Введите целое число: "; cin >> i; Number *object = new Number(i); Command *commands[3]; commands[1] = new Command(object, &Number::dubble); commands[2] = new Command(object, &Number::half); cout << "Выход[0], Удвоить[1], Разделить на 2[2]:"; cin >> i; while (i) { switch (i) { case 0: break; case 1: case 2: commands[i]->execute(); case 3: Command::undo(); break; case 4: Command::redo(); break; 76 break; default: break; } cout << " " << object->getValue() << endl; cout << "Выход[0], Удвоить[1], Разделить на 2[2], Отменить[3], Вернуть[4]: "; cin >> i; } } Особенности паттерна Memento • Паттерны Command и Memento определяют объекты "волшебная палочка", которые передаются от одного владельца к другому и используются позднее. В Command такой "волшебной палочкой" является запрос; в Memento - внутреннее состояние объекта в некоторый момент времени. Полиморфизм важен для Command, но не важен для Memento потому, что интерфейс Memento настолько "узкий", что его можно передавать как значение. • Command может использовать Memento для сохранения состояния, необходимого для выполнения отмены действий. • Memento часто используется совместно с Iterator. Iterator может использовать Memento для сохранения состояния итерации. Паттерн Observer (наблюдатель, издатель-подписчик) Назначение паттерна Observer • Паттерн Observer определяет зависимость "один-ко-многим" между объектами так, что при изменении состояния одного объекта все зависящие от него объекты уведомляются и обновляются автоматически. • Паттерн Observer инкапсулирует главный (независимый) компонент в абстракцию Subject и изменяемые (зависимые) компоненты в иерархию Observer. • Паттерн Observer определяет часть "View" в модели Model-View-Controller (MVC) . Паттерн Observer находит широкое применение в системах пользовательского интерфейса, в которых данные и их представления ("виды") отделены друг от друга. При изменении данных должны быть изменены все представления этих данных (например, в виде таблицы, графика и диаграммы). 77 Решаемая проблема Имеется система, состоящая из множества взаимодействующих классов. При этом взаимодействующие объекты должны находиться в согласованных состояниях. Вы хотите избежать монолитности такой системы, сделав классы слабо связанными (или повторно используемыми). Обсуждение паттерна Observer Паттерн Observer определяет объект Subject, хранящий данные (модель), а всю функциональность "представлений" делегирует слабосвязанным отдельным объектам Observer. При создании наблюдатели Observer регистрируются у объекта Subject. Когда объект Subject изменяется, он извещает об этом всех зарегистрированных наблюдателей. После этого каждый обозреватель запрашивает у объекта Subject ту часть состояния, которая необходима для отображения данных. Такая схема позволяет динамически настраивать количество и "типы" представлений объектов. Описанный выше протокол взаимодействия соответствует модели вытягивания (pull), когда субъект информирует наблюдателей о своем изменении, и каждый наблюдатель ответственен за "вытягивание" у Subject нужных ему данных. Существует также модель проталкивания, когда субъект Subject посылает ("проталкивает") наблюдателям детальную информацию о своем изменении. Паттерн Observer впервые был применен в архитектуре Model-View-Controller языка Smalltalk, представляющей каркас для построения пользовательских интерфейсов. Структура паттерна Observer Subject представляет главную (независимую) абстракцию. Observer представляет изменяемую (зависимую) абстракцию. Субъект извещает наблюдателей о своем изменении, на что каждый наблюдатель может запросить состояние субъекта. 78 UML-диаграмма классов паттерна Observer Пример паттерна Observer Паттерн Observer определяет зависимость "один-ко-многим" между объектами так, что при изменении состояния одного объекта все зависящие от него объекты уведомляются и обновляются автоматически. // App15_Observer_pattern.cpp : Этот файл содержит функцию "main". // Здесь начинается и заканчивается выполнение программы. // // Реализация классов паттерна наблюдатель #include "pch.h" #include #include using namespace std; //class Observer; // 1. "Независимая" функциональность class Subject { // 3. Связь только c базовым классом Observer vector < class Observer * > views; int value; public: void attach(Observer *obs) { views.push_back(obs); } void setVal(int val) { value = val; notify(); } int getVal() { return value; } void notify(); }; // 2. "Зависимая" функциональность class Observer { Subject *model; int denom; public: Observer(Subject *mod, int div) { model = mod; denom = div; // 4. Наблюдатели регистрируются у субъекта model->attach(this); 79 } virtual void update() = 0; protected: Subject *getSubject() { return model; } int getDivisor() { return denom; } }; void Subject::notify() { // 5. Извещение наблюдателей for (size_t i = 0; i < views.size(); i++) views[i]->update(); } class DivObserver : public Observer { public: DivObserver(Subject *mod, int div) : Observer(mod, div) {} void update() { // 6. "Вытягивание" интересующей информации int v = getSubject()->getVal(), d = getDivisor(); cout << v << " div " << d << " is " << v / d << '\n'; } }; class ModObserver : public Observer { public: ModObserver(Subject *mod, int div) : Observer(mod, div) {} void update() { // 6. "Вытягивание" интересующей информации int v = getSubject()->getVal(), d = getDivisor(); cout << v << " mod " << d << " is " << v % d << '\n'; } }; int main() { Subject subj; DivObserver divObs1(&subj, 4); // 7. Клиент настраивает число DivObserver divObs2(&subj, 3); // и типы наблюдателей ModObserver modObs3(&subj, 3); subj.setVal(12); } Использование паттерна Observer 1. Проведите различия между основной (или независимой) и дополнительной (или зависимой) функциональностями. 2. Смоделируйте "независимую" функциональность с помощью абстракции "субъект". 3. Смоделируйте "зависимую" функциональность с помощью иерархии "наблюдатель". 4. Класс Subject связан только c базовым классом Observer. 5. Клиент настраивает количество и типы наблюдателей. 6. Наблюдатели регистрируются у субъекта. 7. Субъект извещает всех зарегистрированных наблюдателей. 8. Субъект может "протолкнуть" информацию в наблюдателей, или наблюдатели могут "вытянуть" необходимую им информацию от объекта Subject. 80 Особенности паттерна Observer • Паттерны Chain of Responsibility, Command, Mediator и Observer показывают, как можно разделить отправителей и получателей запросов с учетом своих особенностей. Chain of Responsibility передает запрос отправителя по цепочке потенциальных получателей. Command определяет связь - "оправитель-получатель" с помощью подкласса. В Mediator отправитель и получатель ссылаются друг на друга косвенно, через объект-посредник. В паттерне Observer связь между отправителем и получателем получается слабой, при этом число получателей может конфигурироваться во время выполнения. • Mediator и Observer являются конкурирующими паттернами. Если Observer распределяет взаимодействие c помощью объектов "наблюдатель" и "субъект", то Mediator использует объект-посредник для инкапсуляции взаимодействия между другими объектами. Мы обнаружили, что легче сделать повторно используемыми Наблюдателей и Субъектов, чем Посредников. • Mediator может использовать Observer для динамической регистрации коллег и их взаимодействия с посредником. Реализация паттерна Observer Реализация паттерна Observer по шагам 1. 2. 3. 4. 5. 6. 7. Смоделируйте "независимую" функциональность с помощью абстракции "субъект". Смоделируйте "зависимую" функциональность с помощью иерархии "наблюдатель". Класс Subject связан только c базовым классом Observer. Наблюдатели регистрируются у субъекта. Субъект извещает всех зарегистрированных наблюдателей. Наблюдатели "вытягивают" необходимую им информацию от объекта Subject. Клиент настраивает количество и типы наблюдателей. Паттерн State (состояние) Назначение паттерна State • Паттерн State позволяет объекту изменять свое поведение в зависимости от внутреннего состояния. Создается впечатление, что объект изменил свой класс. • Паттерн State является объектно-ориентированной реализацией конечного автомата. Решаемая проблема Поведение объекта зависит от его состояния и должно изменяться во время выполнения программы. Такую схему можно реализовать, применив множество условных операторов: на основе анализа текущего состояния объекта предпринимаются определенные действия. Однако при большом числе состояний условные операторы будут разбросаны по всему коду, и такую программу будет трудно поддерживать. 81 Обсуждение паттерна State Паттерн State решает указанную проблему следующим образом: Вводит класс Context, в котором определяется интерфейс для внешнего мира. • Вводит абстрактный класс State. • Представляет различные "состояния" конечного автомата в виде подклассов State. • В классе Context имеется указатель на текущее состояние, который изменяется при изменении состояния конечного автомата. • Паттерн State не определяет, где именно определяется условие перехода в новое состояние. Существует два варианта: класс Context или подклассы State. Преимущество последнего варианта заключается в простоте добавления новых производных классов. Недостаток заключается в том, что каждый подкласс State для осуществления перехода в новое состояние должен знать о своих соседях, что вводит зависимости между подклассами. Существует также альтернативный таблично-ориентированный подход к проектированию конечных автоматов, основанный на использовании таблицы однозначного отображения входных данных на переходы между состояниями. Однако этот подход обладает недостатками: трудно добавить выполнение действий при выполнении переходов. Подход, основанный на использовании паттерна State, для осуществления переходов между состояниями использует код (вместо структур данных), поэтому эти действия легко добавляемы. Структура паттерна State Класс Context определяет внешний интерфейс для клиентов и хранит внутри себя ссылку на текущее состояние объекта State. Интерфейс абстрактного базового класса State повторяет интерфейс Context за исключением одного дополнительного параметра - указателя на экземпляр Context. Производные от State классы определяют поведение, специфичное для конкретного состояния. Класс "обертка" Context делегирует все полученные запросы объекту "текущее состояние", который может использовать полученный дополнительный параметр для доступа к экземпляру Context. 82 UML-диаграмма классов паттерна State // App16_State_pattern.cpp : Этот файл содержит функцию "main". Здесь начинается и заканчивается выполнение программы. // // пример реализации паттерна State // пример конечного автомата с двумя возможными состояниями и двумя событиями. #include "pch.h" #include using namespace std; class Machine; class State { public: virtual void on(Machine *m) { cout << "уже в ON" << endl; } virtual void off(Machine *m) { cout << "уже в OFF" << endl; } }; class Machine // Context { State *current;// указатель на текущее состояние public: Machine(); void setCurrent(State *s) { current = s; } void on() { current->on(this); } void off() { current->off(this); } }; class ON : public State { public: ON() { cout << " ON::ON() "; }; ~ON() { cout << " ON::~ON()\n"; }; void off(Machine *m); }; class OFF : public State { public: OFF() { cout << " OFF::OFF() "; }; 83 ~OFF() { cout << " OFF::~OFF()\n"; }; void on(Machine *m) { cout << "переход из OFF в ON"; m->setCurrent(new ON()); delete this; } }; void ON::off(Machine *m) { cout << "переход из ON в OFF"; m->setCurrent(new OFF()); delete this; } Machine::Machine() { current = new OFF(); cout << '\n'; } int main() { void(Machine:: *ptrs[])() = { &Machine::off, &Machine::on }; Machine fsm; int num; setlocale(LC_ALL, "rus"); while (1) { cout << "Введите 0|1: "; cin >> num; if (num == 0 || num == 1) (fsm.*ptrs[num])(); else break; } } Использование паттерна State • Определите существующий или создайте новый класс-"обертку" Context, который будет использоваться клиентом в качестве "конечного автомата". 84 • • • • • Создайте базовый класс State, который повторяет интерфейс класса Context. Каждый метод принимает один дополнительный параметр: экземпляр класса Context. Класс State может определять любое полезное поведение "по умолчанию". Создайте производные от State классы для всех возможных состояний. Класс-"обертка" Context имеет ссылку на объект "текущее состояние". Все полученные от клиента запросы класс Context просто делегирует объекту "текущее состояние", при этом в качестве дополнительного параметра передается адрес объекта Context. Используя этот адрес, в случае необходимости методы класса State могут изменить "текущее состояние" класса Context. Особенности паттерна State • • • • • Объекты класса State часто бывают одиночками. Flyweight показывает, как и когда можно разделять объекты State. Паттерн Interpreter может использовать State для определения контекстов при синтаксическом разборе. Паттерны State и Bridge имеют схожие структуры за исключением того, что Bridge допускает иерархию классов-конвертов (аналогов классов-"оберток"), а State-нет. Эти паттерны имеют схожие структуры, но решают разные задачи: State позволяет объекту изменять свое поведение в зависимости от внутреннего состояния, в то время как Bridge разделяет абстракцию от ее реализации так, что их можно изменять независимо друг от друга. Реализация паттерна State основана на паттерне Strategy. Различия заключаются в их назначении. Паттерн Chain of Responsibility (цепочка обязанностей) Назначение паттерна Chain of Responsibility • • • Паттерн Chain of Responsibility позволяет избежать жесткой зависимости отправителя запроса от его получателя, при этом запрос может быть обработан несколькими объектами. Объектыполучатели связываются в цепочку. Запрос передается по этой цепочке, пока не будет обработан. Вводит конвейерную обработку для запроса с множеством возможных обработчиков. Объектно-ориентированный связанный список с рекурсивным обходом. Решаемая проблема Имеется поток запросов и переменное число "обработчиков" этих запросов. Необходимо эффективно обрабатывать запросы без жесткой привязки к их обработчикам, при этом запрос может быть обработан любым обработчиком. 85 Обсуждение паттерна Chain of Responsibility Паттерн инкапсулирует элементы по обработке запросов внутри абстрактного "конвейера". Клиенты "кидают" свои запросы на вход этого конвейера. Паттерн Chain of Responsibility связывает в цепочку объекты-получатели, а затем передает запроссообщение от одного объекта к другому до тех пор, пока не достигнет объекта, способного его обработать. Число и типы объектов-обработчиков заранее неизвестны, они могут настраиваться динамически. Механизм связывания в цепочку использует рекурсивную композицию, что позволяет использовать неограниченное число обработчиков. Паттерн Chain of Responsibility упрощает взаимосвязи между объектами. Вместо хранения ссылок на всех кандидатов-получателей запроса, каждый отправитель хранит единственную ссылку на начало цепочки, а каждый получатель имеет единственную ссылку на своего преемника - последующий элемент в цепочке. !!! Убедитесь, что система корректно "отлавливает" случаи необработанных запросов. Не используйте паттерн Chain of Responsibility, когда каждый запрос обрабатывается только одним обработчиком, или когда клиент знает, какой именно объект должен обработать его запрос. Структура паттерна Chain of Responsibility Производные классы знают, как обрабатывать запросы клиентов. Если "текущий" объект не может обработать запрос, то он делегирует его базовому классу, который делегирует "следующему" объекту и так далее. 86 UML-диаграмма классов паттерна Chain of Responsibility Обработчики могут вносить свой вклад в обработку каждого запроса. Запрос может быть передан по всей длине цепочки до самого последнего звена. Пример паттерна Chain of Responsibility Паттерн Chain of Responsibility позволяет избежать привязки отправителя запроса к его получателю, давая шанс обработать запрос нескольким получателям. Банкомат использует Chain of Responsibility в механизме выдачи денег. Использование паттерна Chain of Responsibility • • • • • • Базовый класс имеет указатель на "следующий обработчик". Каждый производный класс реализует свой вклад в обработку запроса. Если запрос должен быть "передан дальше", то производный класс "вызывает" базовый класс, который с помощью указателя делегирует запрос далее. Клиент (или третья сторона) создает цепочку получателей (которая может иметь ссылку с последнего узла на корневой узел). Клиент передает каждый запрос в начало цепочки. Рекурсивное делегирование создает иллюзию волшебства. // App17_Chain of Responsibility_pattern.cpp : // Этот файл содержит функцию "main". Здесь начинается и заканчивается выполнение программы. // //реализация паттерна Цепочка обязанностей #include "pch.h" 87 #include #include #include using namespace std; class Base { // 1. Указатель "next" в базовом классе Base *next; public: Base() { next = 0; } void setNext(Base *n) { next = n; } void add(Base *n) { if (next) next->add(n); else next = n; } // 2. Метод базового класса, делегирующий запрос next-объекту virtual void handle(int i) { next->handle(i); } }; class Handler1 : public Base { public: void handle(int i) { if (rand() % 3) { cout << "H1 запрос отдан экземпляру базового класса " << i << " Base::handle(i); } else cout << "H1 запрос обработан " << i << " "; } }; class Handler2 : public Base { public: void handle(int i) { if (rand() % 3) { cout << "H2 запрос отдан экземпляру базового класса " << i << " Base::handle(i); } else cout << "H2 запрос обработан " << i << " "; } }; class Handler3 : public Base { public: void handle(int i) { if (rand() % 3) { cout << "H3 запрос отдан экземпляру базового класса " << i << " Base::handle(i); "; "; "; 88 } else cout << "H3 запрос обработан " << i << " "; } }; int main() { setlocale(LC_ALL, "rus"); srand((unsigned int)time(nullptr)); // инициализация датчика случайных чисел Handler1 root; Handler2 two; Handler3 thr; root.add(&two); root.add(&thr); thr.setNext(&root); // замыкаю обработку по кругу for (int i = 1; i < 10; i++) { root.handle(i); cout << '\n'; } cin.get(); } Особенности паттерна Chain of Responsibility • • • Паттерны Chain of Responsibility, Command, Mediator и Observer показывают, как можно разделить отправителей и получателей с учетом их особенностей. Chain of Responsibility передает запрос отправителя по цепочке потенциальных получателей. Chain of Responsibility может использовать Command для представления запросов в виде объектов. Chain of Responsibility часто применяется вместе с паттерном Composite. Родитель компонента может выступать в качестве его преемника. 89 Паттерн Interpreter (интерпетатор) Назначение паттерна Interpreter Для заданного языка определяет представление его грамматики, а также интерпретатор предложений этого языка. • Отображает проблемную область в язык, язык – в грамматику, а грамматику – в иерархии объектно-ориентированного проектирования. • Решаемая проблема Пусть в некоторой, хорошо определенной области периодически случается некоторая проблема. Если эта область может быть описана некоторым “языком“, то проблема может быть легко решена с помощью “интерпретирующей машины“. Обсуждение паттерна Interpreter Паттерн Interpreter определяет грамматику простого языка для проблемной области, представляет грамматические правила в виде языковых предложений и интерпретирует их для решения задачи. Для представления каждого грамматического правила паттерн Interpreter использует отдельный класс. А так как грамматика, как правило, имеет иерархическую структуру, то иерархия наследования классов хорошо подходит для ее описания. Абстрактный базовый класс определяет метод interpret(), принимающий (в качестве аргумента) текущее состояние языкового потока. Каждый конкретный подкласс реализует метод interpret(), добавляя свой вклад в процесс решения проблемы. Структура паттерна Interpreter Паттерн Interpreter моделирует проблемную область с помощью рекурсивной грамматики. Каждое грамматическое правило может быть либо составным (правило ссылается на другие правила) либо терминальным (листовой узел в структуре ”дерево”). Для рекурсивного обхода ”предложений” при их интерпретации используется паттерн Composite. 90 UML-диаграмма классов паттерна Interpreter Использование паттерна Interpreter 1. 2. 3. 4. 5. 6. Определите “малый“ язык, “инвестиции” в который будут оправданными. Разработайте грамматику для языка. Для каждого грамматического правила (продукции) создайте свой класс. Полученный набор классов организуйте в структуру с помощью паттерна Composite. В полученной иерархии классов определите метод interpret(Context). Объект Context инкапсулирует информацию, глобальную по отношению к интерпретатору. Используется классами во время процесса ”интерпретации”. Особенности паттерна Interpreter • • • • Абстрактное синтаксическое дерево интерпретатора – пример паттерна Composite. Для обхода узлов дерева может применяться паттерн Iterator. Терминальные символы могут разделяться c помощью Flyweight. Паттерн Interpreter не рассматривает вопросы синтаксического разбора. Когда грамматика очень сложная, должны использоваться другие методики. Пример использования: Рассмотрим задачу интерпретирования (вычисления) значений строковых представлений римских чисел. Используем следующую грамматику: romanNumeral ::= {thousands} {hundreds} {tens} {ones} thousands,hundreds,tens,ones ::= nine | four | {five} {one} {one} {one} nine ::= "CM" | "XC" | "IX" four ::= "CD" | "XL" | "IV" five ::= 'D' | 'L' | 'V' one ::= 'M' | 'C' | 'X' | 'I' Для проверки и интерпретации строки используется иерархия классов с общим базовым классом RNInterpreter, имеющим 4 под-интерпретатора. Каждый под-интерпретатор получает "контекст" 91 (оставшуюся неразобранную часть строки и накопленное вычисленное значение разобранной части) и вносит свой вклад в процесс обработки. Под-переводчики просто определяют шаблонные методы, объявленные в базовом классе RNInterpreter. Пример использования: App61 Оригинал от Чуприкова #include "pch.h" #include using namespace std; // Совместное использование паттернов Interpreter и Template Method // опережающие объявления классов // опережающие объявления классов class Thousand; class Hundred; class Ten; class One; class RNInterpreter { public: RNInterpreter(); // конструктор для клиента RNInterpreter(int) {} // конструктор для классов-наследников предотвращающий бесконечный цикл int solve(char*); // interpret() для клиента virtual void solve(char *input, int &total); protected: // эти члены-функции нельзя делать чисто-виртуальными virtual char one() { return '\0'; } virtual char *four() { return '\0'; } virtual char five() { return '\0'; } virtual char *nine() { return '\0'; } virtual int multiplier() { return '\0'; } private: RNInterpreter *thousands; RNInterpreter *hundreds; RNInterpreter *tens; RNInterpreter *ones; }; void RNInterpreter::solve(char *input, int &total) // { // выполняется разбор входной строки int index = 0; if (!strncmp(input, nine(), 2)) // если 9 { total += 9 * multiplier(); index += 2; } else if (!strncmp(input, four(), 2)) // если 4 { total += 4 * multiplier(); index += 2; } else { if (input[0] == five()) // если 5 { total += 5 * multiplier(); index = 1; } else index = 0; for (int end = index + 3; index < end; index++) if (input[index] == one()) total += 1 * multiplier(); 92 else break; } strcpy(input, &(input[index]));// удаляю из входной строки обработанные символы } class Thousand : public RNInterpreter { public: // определение конструктора с 1 аргументом для предотв. бесконечного цикла в конструкторе базового класса Thousand(int) : RNInterpreter(1) {} protected: char one() { return 'M'; } char *four() { return ""; } char five() { return '\0'; } char *nine() { return ""; } int multiplier() { return 1000; } }; class Hundred : public RNInterpreter { public: Hundred(int) : RNInterpreter(1) {} protected: char one() { return 'C'; } char *four() { return "CD"; } char five() { return 'D'; } char *nine() { return "CM"; } int multiplier() { return 100; } }; class Ten : public RNInterpreter { public: Ten(int) : RNInterpreter(1) {} protected: char one() { return 'X'; } char *four() { return "XL"; } char five() { return 'L'; } char *nine() { return "XC"; } int multiplier() { return 10; } }; class One : public RNInterpreter { public: One(int) : RNInterpreter(1) {} protected: char one() { return 'I'; } char *four() { return "IV"; } char five() { return 'V'; } char *nine() { return "IX"; } int multiplier() { return 1; } }; RNInterpreter::RNInterpreter() { thousands = new Thousand(1); hundreds = new Hundred(1); tens = new Ten(1); ones = new One(1); } int RNInterpreter::solve(char *input) { int total = 0; thousands->solve(input, total); hundreds->solve(input, total); 93 tens->solve(input, total); ones->solve(input, total); if (strcmp(input, "")) return 0; return total; } int main() { RNInterpreter interpreter; char input[20]; setlocale(LC_ALL, "rus"); cout << "Введите число в римской записи: "; while (cin >> input) { cout << input; cout << " = " << interpreter.solve(input) << endl; cout << "Введите число в римской записи: "; } } Паттерн Strategy (стратегия) Назначение паттерна Strategy Переносит алгоритмы в отдельную иерархию классов, делая их взаимозаменяемыми Существуют системы, поведение которых может определяться согласно одному алгоритму из некоторого семейства. Все алгоритмы этого семейства являются родственными: предназначены для решения общих задач, имеют одинаковый интерфейс для использования и отличаются только реализацией (поведением). Пользователь, предварительно настроив программу на нужный алгоритм (выбрав стратегию), получает ожидаемый результат. Как пример, - приложение, предназначенное для компрессии файлов использует один из доступных алгоритмов: zip, arj или rar. Объектно-ориентированный дизайн такой программы может быть построен на идее использования полиморфизма. В результате получаем набор родственных классов с общим интерфейсом и различными реализациями алгоритмов. Представленному подходу свойственны следующие недостатки: 1. Реализация алгоритма жестко привязана к его подклассу, что затрудняет поддержку и расширение такой системы. 2. Система, построенная на основе наследования, является статичной. Заменить один алгоритм на другой в ходе выполнения программы уже невозможно. Применение паттерна Strategy позволяет устранить указанные недостатки. Описание паттерна Strategy Паттерн Strategy переносит в отдельную иерархию классов все детали, связанные с реализацией алгоритмов. Для случая программы сжатия файлов абстрактный базовый класс Compression этой иерархии объявляет интерфейс, общий для всех алгоритмов и используемый классом Compressor. Подклассы ZIP_Compression, ARJ_Compression и RAR_Compression его реализуют в соответствии с тем или иным алгоритмом. Класс Compressor содержит указатель на объект абстрактного типа Compression и предназначен для переадресации пользовательских запросов конкретному алгоритму. Для замены одного алгоритма другим достаточно перенастроить этот указатель на объект нужного типа. 94 Структура паттерна Strategy UML-диаграмма классов паттерна Strategy // App19_Strategy_pattern.cpp : Этот файл содержит функцию "main". Здесь начинается и заканчивается выполнение программы. // #include "pch.h" #include #include using namespace std; // Иерархия классов, определяющая алгоритмы сжатия файлов class Compression { public: virtual ~Compression() {} virtual void compress(const string & file) = 0; }; class ZIP_Compression : public Compression { public: void compress(const string & file) { cout << "ZIP compression" << endl; } }; class ARJ_Compression : public Compression { public: void compress(const string & file) { cout << "ARJ compression" << endl; } }; class RAR_Compression : public Compression { public: void compress(const string & file) { cout << "RAR compression" << endl; } }; 95 // Класс для использования class Compressor { public: Compressor(Compression* comp) : p(comp) {} ~Compressor() { delete p; } void compress(const string & file) { p->compress(file); } private: Compression* p; }; int main() { Compressor* p; p = new Compressor(new ZIP_Compression); p->compress("file.txt"); delete p; p = new Compressor(new ARJ_Compression); p->compress("file.txt"); delete p; p = new Compressor(new RAR_Compression); p->compress("file.txt"); delete p; return 0; } Результаты применения паттерна Strategy Достоинства паттерна Strategy • • • Систему проще поддерживать и модифицировать, так как семейство алгоритмов перенесено в отдельную иерархию классов. Паттерн Strategy предоставляет возможность замены одного алгоритма другим в процессе выполнения программы. Паттерн Strategy позволяет скрыть детали реализации алгоритмов от клиента. Недостатки паттерна Strategy • • Для правильной настройки системы пользователь должен знать об особенностях всех алгоритмов. Число классов в системе, построенной с применением паттерна Strategy, возрастает. Паттерн Template Method (шаблонный метод) Определяет шаги алгоритма, позволяя подклассам изменить некоторые из них. Назначение паттерна Template Method • • Паттерн Template Method определяет основу алгоритма и позволяет подклассам изменить некоторые шаги этого алгоритма без изменения его общей структуры. Базовый класс определяет шаги алгоритма с помощью абстрактных операций, а производные классы их реализуют. 96 Решаемая проблема Имеются два разных, но в тоже время очень похожих компонента. Вы хотите внести изменения в оба компонента, избежав дублирования кода. Обсуждение паттерна Template Method Проектировщик компонента решает, какие шаги алгоритма являются неизменными (или стандартными), а какие изменяемыми (или настраиваемыми). Абстрактный базовый класс реализует стандартные шаги алгоритма и может предоставлять (или нет) реализацию по умолчанию для настраиваемых шагов. Изменяемые шаги могут (или должны) предоставляться клиентом компонента в конкретных производных классах. Проектировщик компонента определяет необходимые шаги алгоритма, порядок их выполнения, но позволяет клиентам компонента расширять или замещать некоторые из этих шагов. Паттерн Template Method широко применяется в каркасах приложений (frameworks). Каждый каркас реализует неизменные части архитектуры в предметной области, а также определяет те части, которые могут или должны настраиваться клиентом. Таким образом, каркас приложения становится "центром вселенной", а настройки клиента являются просто "третьей планетой от Солнца". Эту инвертированную структуру кода ласково называют принципом Голливуда - "Не звоните нам, мы сами вам позвоним". Структура паттерна Template Method UML-диаграмма классов паттерна Template Method Реализация метода templateMethod() вызывает методы stepOne(), stepTwo() и stepThree(). Метод stepTwo() является "замещающим" методом. Он объявлен в базовом классе, а определяется в производных классах. Каркасы приложений широко используют паттерн Тemplate Method. Весь повторно используемый код определяется в базовых классах каркаса, нужное поведение системы клиенты определяют в создаваемых производных классах. 97 Пример паттерна Template Method Паттерн Template Method определяет основу алгоритма и позволяет подклассам изменить некоторые шаги этого алгоритма без изменения его общей структуры. Строители зданий используют шаблонный метод при проектировании новых домов. Здесь могут использоваться уже существующие типовые планы, в которых модифицируются только отдельные части. Использование паттерна Template Method 1. Исследуйте алгоритм и решите, какие шаги являются стандартными, а какие должны определяться подклассами. 2. Создайте новый абстрактный базовый класс, в котором будет реализован принцип "не звоните 3. 4. 5. 6. нам, мы сами вам позвоним". Поместите в новый класс основу алгоритма (шаблонный метод) и определения стандартных шагов. Для каждого шага, требующего различные реализации, определите "замещающий" виртуальный метод. Этот метод может иметь реализацию по умолчанию или быть чисто виртуальным. Вызовите "замещающий" метод из шаблонного метода. Создайте подклассы от нового абстрактного базового класса и реализуйте в них "замещающие" методы. 98 // App20_Template method_pattern.cpp : Этот файл содержит функцию "main". // Здесь начинается и заканчивается выполнение программы. // // Реализация паттерна Шаблонный метод #include "pch.h" #include using namespace std; class Base { void a() { cout << "Base a" << endl; } void c() { cout << "Base c" << endl; } void e() { cout << "Base e" << endl; } // 2. Для шагов, требующих особенной реализации, определите "замещающие" методы. virtual void ph1() = 0; virtual void ph2() = 0; public: // 1. Стандартизуйте основу алгоритма в шаблонном методе базового класса void execute() { a(); ph1(); c(); ph2(); e(); } }; class One : public Base { // 3. Производные классы реализуют "замещающие" методы. /*virtual*/void ph1() { cout << "One b" << endl; } /*virtual*/void ph2() { cout << "One d" << endl; } }; class Two : public Base { /*virtual*/void ph1() { cout << "Two 2" << endl; } /*virtual*/void ph2() { cout << "Two 4" << endl; } }; int main() { Base *array[] = { &(One()), &(Two()) }; for (int i = 0; i < 2; i++) { array[i]->execute(); cout << "----------" << endl; } } 99 Особенности паттерна Template Method • • • Template Method использует наследование для модификации части алгоритма. Strategy использует делегирование для модификации всего алгоритма. Strategy изменяет логику отдельных объектов. Template Method изменяет логику всего класса. Factory Methods (Фабричные методы) часто вызываются из шаблонных методов. Реализация паттерна Template Method 1. Стандартизуйте основу алгоритма в шаблонном методе базового класса. 2. Для шагов, требующих особенной реализации, определите "замещающие" методы. 3. Производные классы реализуют "замещающие" методы. Паттерн Visitor (посетитель) Определяет новую операцию в классе без его изменения Назначение паттерна Visitor • • • • Паттерн Visitor определяет операцию, выполняемую на каждом элементе из некоторой структуры. Позволяет, не изменяя классы этих объектов, добавлять в них новые операции. Является классической техникой для восстановления потерянной информации о типе. Паттерн Visitor позволяет выполнить нужные действия в зависимости от типов двух объектов. Предоставляет механизм двойной диспетчеризации. Решаемая проблема Различные и несвязанные операции должны выполняться над узловыми объектами некоторой гетерогенной совокупной структуры. Вы хотите избежать "загрязнения" классов этих узлов такими операциями (то есть избежать добавления соответствующих методов в эти классы). И вы не хотите запрашивать тип каждого узла и осуществлять приведение указателя к правильному типу, прежде чем выполнить нужную операцию. Обсуждение паттерна Visitor Основным назначением паттерна Visitor является введение абстрактной функциональности для совокупной иерархической структуры объектов "элемент", а именно, паттерн Visitor позволяет, не изменяя классы Element, добавлять в них новые операции. Для этого вся обрабатывающая функциональность переносится из самих классов Element (эти классы становятся "легковесными") в иерархию наследования Visitor. При этом паттерн Visitor использует технику "двойной диспетчеризации". Обычно при передаче запросов используется "одинарная диспетчеризация" – то, какая операция будет выполнена для обработки запроса, зависит от имени запроса и типа получателя. В "двойной диспетчеризации" вызываемая операция зависит от имени запроса и типов двух получателей (типа Visitor и типа посещаемого элемента Element). Реализуется паттерн Visitor следующим образом. Создается иерархия классов Visitor, в абстрактном базовом классе которой для каждого подкласса Element совокупной структуры определяется чисто виртуальный метод visit(). Каждый метод visit() принимает один аргумент - указатель или ссылку на подкласс Element. Каждая новая добавляемая операция моделируется при помощи конкретного подкласса Visitor. Подклассы Visitor реализуют visit() методы, объявленные в базовом классе Visitor. 100 Добавляется один чисто виртуальный метод accept() в базовый класс иерархии Element. В качестве параметра accept() принимает единственный аргумент - указатель или ссылку на абстрактный базовый класс иерархии Visitor. Каждый конкретный подкласс Element реализует метод accept() следующим образом: используя полученный в качестве параметра адрес экземпляра подкласса Visitor, просто вызывает его метод visit(), передавая в качестве единственного параметра указатель this. Теперь "элементы" и "посетители" готовы. Если клиенту нужно выполнить какую-либо операцию, то он создает экземпляр объекта соответствующего подкласса Visitor и вызывает accept() метод для каждого объекта Element, передавая экземпляр Visitor в качестве параметра. При вызове метода accept() ищется правильный подкласс Element. Затем, при вызове метода visit() управление передается правильному подклассу Visitor. Таким образом, двойная диспетчеризация получается как сумма одинарных диспетчеризаций сначала в методе accept(), а затем в методе visit(). Паттерн Visitor позволяет легко добавлять новые операции – нужно просто добавить новый производный от Visitor класс. Однако паттерн Visitor следует использовать только в том случае, если подклассы Element совокупной иерархической структуры остаются стабильными (неизменяемыми). В противном случае, нужно приложить значительные усилия на обновление всей иерархии Visitor. Иногда приводятся возражения по поводу использования паттерна Visitor, поскольку он разделяет данные и алгоритмы, что противоречит концепции объектно-ориентированного программирования. Однако успешный опыт применения STL, где разделение данных и алгоритмов положено в основу, доказывает возможность использования паттерна Visitor. Структура паттерна Visitor Несмотря на то, что реализации метода accept() в подклассах иерархии Element всегда одинаковая, этот метод не может быть перенесен в базовый класс Element и наследоваться производными классами. В этом случае адрес, получаемый с помощью указателя this, будет всегда соответствовать базовому типу Element. UML-диаграмма классов паттерна Visitor Пример паттерна Visitor Паттерн Visitor определяет операцию, выполняемую на каждом элементе из некоторой структуры без изменения классов этих объектов. Таксомоторная компания использует этот паттерн в своей работе. 101 Когда клиент звонит в такую компанию, диспетчер отправляет к нему свободное такси. После того как клиент садится в такси, его доставляют до места. Использование паттерна Visitor 1. Убедитесь, что текущая иерархия Element будет оставаться стабильной и, что открытый 2. 3. 4. 5. 6. интерфейс этих классов достаточно эффективен для доступа классов Visitor. Если это не так, то паттерн Visitor – не очень хорошее решение. Создайте базовый класс Visitor c методами visit(ElementXxx) для каждого подкласса Element. Добавьте метод accept(Visitor) в иерархию Element. Реализация этого метода во всех подклассах Element всегда одна и та же – accept( Visitor v ) { v.visit( this ); }. Из-за циклических зависимостей объявления классов Element и Visitor должны чередоваться. Иерархия Element связана только с базовым классом Visitor, в то время как иерархия Visitor связана с каждым производным от Element классом. Если стабильность иерархии Element низкая, а стабильность иерархии Visitor высокая, рассмотрите возможность обмена "ролей" этих двух иерархий. Для каждой "операции", которая должна выполняться для объектов Element, создайте производный от Visitor класс. Реализации метода visit() должны использовать открытый интерфейс класса Element. Клиенты создают объекты Visitor и передают их каждому объекту Element, вызывая accept(). Особенности паттерна Visitor Совокупная структура объектов Elements может определяться с помощью паттерна Composite. • Для обхода Composite может использоваться Iterator. • Паттерн Visitor демонстрирует классический прием восстановления информации о потерянных типах, не прибегая к понижающему приведению типов (dynamic cast). • Реализация паттерна Visitor Реализация паттерна Visitor по шагам 1. Добавьте метод accept(Visitor) иерархию "элемент". 102 Создайте базовый класс Visitor и определите методы visit() для каждого типа "элемента". 3. Создайте производные классы Visitor для каждой "операции", исполняемой над "элементами". 4. Клиент создает объект Visitor и передает его в вызываемый метод accept(). 2. // App21_Visitor_pattern.cpp : Этот файл содержит функцию "main". // Здесь начинается и заканчивается выполнение программы. // // реализация паттерна визитер #include "pch.h" #include #include using namespace std; // 1. Добавьте метод accept(Visitor) в иерархию "элемент" class Element { public: virtual void accept(class Visitor &v) = 0; }; class This : public Element { public: /*virtual*/void accept(Visitor &v); string thiss() { return "This"; } }; class That : public Element { public: /*virtual*/void accept(Visitor &v); string that() { return "That"; } }; class TheOther : public Element { public: /*virtual*/void accept(Visitor &v); string theOther() { return "TheOther"; } }; // 2. Создайте базовый класс Visitor и определите // методы visit()для каждого типа "элемента" class Visitor { public: virtual void visit(This *e) = 0; virtual void visit(That *e) = 0; virtual void visit(TheOther *e) = 0; }; /*virtual*/void This::accept(Visitor &v) { v.visit(this); } /*virtual*/void That::accept(Visitor &v) { v.visit(this); } /*virtual*/void TheOther::accept(Visitor &v) { v.visit(this); } // 3. Создайте производные классы Visitor для каждой // "операции", исполняемой над "элементами" class UpVisitor : public Visitor 103 { /*virtual*/void visit(This *e) { cout << "do Up on " + e->thiss() << endl; } /*virtual*/void visit(That *e) { cout << "do Up on " + e->that() << endl; } /*virtual*/void visit(TheOther *e) { cout << "do Up on " + e->theOther() << endl; } }; class DownVisitor : public Visitor { /*virtual*/void visit(This *e) { cout << "do Down on " + e->thiss() << endl; } /*virtual*/void visit(That *e) { cout << "do Down on " + e->that() << endl; } /*virtual*/void visit(TheOther *e) { cout << "do Down on " + e->theOther() << endl; } }; int main() { int i; Element *list[] = { new This(), new That(), new TheOther() }; UpVisitor up; // 4. Клиент создает DownVisitor down; // объекты Visitor for (i = 0; i < 3; i++) // и передает каждый list[i]->accept(up); for (i = 0; i < 3; i++) // в вызываемый метод accept() list[i]->accept(down); } 104
«Типовые решения для объекто-ориентированных программ. Паттерны (patterns) проектирования» 👇
Готовые курсовые работы и рефераты
Купить от 250 ₽
Решение задач от ИИ за 2 минуты
Решить задачу
Найди решение своей задачи среди 1 000 000 ответов
Найти

Тебе могут подойти лекции

Смотреть все 588 лекций
Все самое важное и интересное в Telegram

Все сервисы Справочника в твоем телефоне! Просто напиши Боту, что ты ищешь и он быстро найдет нужную статью, лекцию или пособие для тебя!

Перейти в Telegram Bot