-
Notifications
You must be signed in to change notification settings - Fork 0
ООП 11. Структурные паттерны: адаптер (Adapter), декоратор (Decorator), компоновщик (Composite), мост (Bridge), заместитель (Proxy), фасад (Facade). Их преимущества и недостатки.
Отличие шаблонов от паттернов: шаблон - конкретная реализация чего-либо, а паттерн - шаблон для решения какой-то задачи (как может решаться данная задача). Паттерн мы всегда адаптируем к своей задаче.
- Мы имеем готовое решение
- За счет готового решения - нюансы все выявлены => надежный код
- Повышается скорость разработки
- Повышается читаемость кода
- Улучшается взаимодействие с коллегами (достаточно сказать название паттерна, который вы используете, и всё сразу станет понятно)
Структурные паттерны предлагают структурные решения: определенную декомпозицию классов использованием схем наследования, включения и прочее.
Проблема: объект в разных местах программы играет разные роли. Это плохо, потому что:
- Для каждой роли разрабатывается свой интерфейс. Несколько ролей для одного объекта - значит, избыточный интерфейс.
- Роль объекта - это возложение на него определенной ответственности. Несколько ролей - это несколько ответственностей. Недопустимо с точки зрения принципов ООП.
Идея решения: У объекта был один интерфейс. Подменяем этот интерфейс другим - соответственно той роли, в которой мы хотим использовать объект. Таким образом, в зависимости от ситуации мы можем использовать этот объект в разных ролях. С этим объектом работаем через объект другого класса. Объект, через класс которого мы работаем, имеет интерфейс необходимой нам роли.
Использование паттерна:
- Один объект может выступать в нескольких ролях.
- Нам нужно встроить в систему сторонние классы, имеющие другой интерфейс. Класс с любым интерфейсом можем встроить в нашу программу.
- Мы, используя полиморфизм, сформировали интерфейс для базового. Определенные сущности, наследуемые от базового класса, должны поддерживать еще какой-то функционал. Мы не можем расширить этот функционал и изменять написанный код. Решаем эту проблему за счет адаптера, который предоставляет расширенный интерфейс.
Диаграмма:
-
Пример кода:
# include <iostream> # include <memory> using namespace std; class Adapter { public: virtual ~Adapter() = 0; virtual void request() = 0; }; Adapter::~Adapter() = default; class BaseAdaptee { public: virtual ~BaseAdaptee() = 0; virtual void specificRequest() = 0; }; BaseAdaptee::~BaseAdaptee() = default; class ConAdapter : public Adapter // подменяет интерфейс { private: shared_ptr<BaseAdaptee> adaptee; public: ConAdapter(shared_ptr<BaseAdaptee> ad) : adaptee(ad) {} virtual void request() override; }; class ConAdaptee : public BaseAdaptee { public: virtual void specificRequest() override { cout << "Method ConAdaptee;" << endl; } }; #pragma region Methods void ConAdapter::request() { cout << "Adapter: "; if (adaptee) { adaptee->specificRequest(); } else { cout << "Empty!" << endl; } } #pragma endregion int main() { shared_ptr<BaseAdaptee> adaptee(new ConAdaptee()); shared_ptr<Adapter> adapter(new ConAdapter(adaptee)); adapter->request(); }
Преимущества
- Он позволяет нам класс с любым интерфейсом использовать в нашей программе.
- Позволяет не создавать нам классы с несколькими ответственностями. Разносим это по другим классам.
- Позволяет расширять интерфейс класса
Есть мини проблемка:
- Интерфейсы могут пересекаться
Проблема: нам надо добавить/подменить классам функционал. Причем одинаковый для нескольких классов. Если мы подменим функционал в производных для каждого из классов, которые мы хотим изменить - разрастается иерархия, приходится дублировать код.
- Картинка с разрастанием иерархии(должна быть стрелочка от базового к B)
Что сразу бросается в глаза, когда мы добавляем одно и то же
- У нас будет повторяющийся код. Мы должны с этим бороться .
- Это приводит к резкому разрастанию всей иерархии наследования. Могут быть другие добавления, а к ним еще.
Идея решения: вынести это добавление в отдельный класс - декоратор.
Использование паттерна: добавление/подмена небольшой части функционала, одинаковой для разных классов.
Преимущества
- Мы получаем крайне гибкую систему, избавляясь от колоссального количества классов. Резко уменьшается иерархия.
- Декорировать можем во время выполнения работы программы.
- Избавляемся от дублирования кода. Этот код уходит в конкретный декоратор, не дублируясь.
Недостатки
- Когда мы сделали сложную обертку, нам, к сожалению, убрать какую-то обёртку будет проблематично. Нам придется заново создавать компонент с обёртками (должен быть ответственный за эту сборку). Мы не можем её вычеркнуть.
- Вызов кучи виртуальных методов замедляет программу.
Диаграмма:
-
Пример кода
# include <iostream> # include <memory> using namespace std; class Component { public: virtual ~Component() = 0; virtual void operation() = 0; }; Component::~Component() = default; class ConComponent : public Component { public: virtual void operation() override { cout << "ConComponent"; } }; class Decorator : public Component { protected: shared_ptr<Component> component; public: Decorator(shared_ptr<Component> comp) : component(comp) {} }; class ConDecorator : public Decorator { public: using Decorator::Decorator; virtual void operation() override; }; #pragma region Method void ConDecorator::operation() { if (component) { component->operation(); cout << "ConDecorator" << endl; } } #pragma endregion int main() { shared_ptr<Component> component(new ConComponent()); shared_ptr<Component> decorator1(new ConDecorator(component)); decorator1->operation(); shared_ptr<Component> decorator2(new ConDecorator(decorator1)); decorator2->operation(); }
Введение
Очень часто мы работаем со многими объектами однотипно, то есть выполняем над ними одни и те же операции. Более того, возникают ситуации, когда нужно выполнять над группой объектов эти действия совместно.
Пример
Есть 3D модель, которую мы можем поворачивать, переносить. А если моделей несколько? У нас возникает необходимость объединять эти 3D модели в одну - сделать сборку и выполнять уже над этой сборкой совместные действия. У нас на сцене объектов может быть много, и естественно хотелось бы рассматривать и каждый в отдельности объект, и совместно.
Идея
Вынести интерфейс, который предлагает контейнер (объект, включающий в себя другие объекты), на уровень базового класса
Компоновщик компонует объекты в древовидную структуру, в которой над всей иерархией объектов можно выполнять такие же действия, как над простым элементом или над его частью.
Почему древовидная структура?
Каждый композит может включать в себя как простые компоненты, так и другие композиты. Получается древовидная структура. Системе неважно, композит это или не композит. Она работает с любым элементом.
Задача методов интерфейса для компонентов - пройтись по списку компонент и выполнить соответствующий метод.
- Базовый класс - Component. Нам должно быть безразлично, с каким объектом мы работаем: то ли это один компонент, то ли это объект, включающий в себя другие объекты (контейнер). Если это контейнер, то нам надо работать с содержимым контейнера, удалять, добавлять в него объекты. Идея - вынести этот интерфейс на уровень базового класса (добавление компонента - add(Component), удаление компонента - remove(Iterator), createIterator()). Нам надо четко понимать, когда мы работаем с каким-то компонентом, чем он является: один объект или контейнер. Для этого нам нужен метод isComposite(). То, что мы можем делать с Component - operation() - чисто виртуальные методы. Остальные (add, remove, и т. д.) мы будем реализовывать.
- Leaf - простой компонент, его задачей будет только реализовать остальные методы - operation.
- Composite - контейнерный класс. Реализует все те методы, что есть в компоненте. Он содержит в себе список компонент.
-
Пример кода
# include <iostream> # include <memory> # include <vector> # include <iterator> using namespace std; class Component; using VectorComponent = vector<shared_ptr<Component>>; using IteratorComponent = VectorComponent::const_iterator; class Component { public: virtual ~Component() = 0; virtual void operation() = 0; virtual bool isComposite() const { return false; } virtual bool add(shared_ptr<Component> comp) { return false; } virtual bool remove(const IteratorComponent& it) { return false; } virtual IteratorComponent begin() const { return IteratorComponent(); } virtual IteratorComponent end() const { return IteratorComponent(); } }; Component::~Component() {} class Figure : public Component { public: virtual void operation() override { cout << "Figure method;" << endl; } }; class Camera : public Component { public: virtual void operation() override { cout << "Camera method;" << endl; } }; class Composite : public Component { private: VectorComponent vec; public: Composite() = default; Composite(shared_ptr<Component> first, ...); virtual void operation() override; virtual bool isComposite() const override { return true; } virtual bool add(shared_ptr<Component> comp) { vec.push_back(comp); return false; } virtual bool remove(const IteratorComponent& it) { vec.erase(it); return false; } virtual IteratorComponent begin() const override { return vec.begin(); } virtual IteratorComponent end() const override { return vec.end(); } }; Composite::Composite(shared_ptr<Component> first, ...) { for (shared_ptr<Component>* ptr = &first; *ptr; ++ptr) vec.push_back(*ptr); } void Composite::operation() { cout << "Composite method:" << endl; for (auto elem : vec) elem->operation(); } int main() { using Default = shared_ptr<Component>; shared_ptr<Component> fig1(new Figure()), fig2(new Figure), cam1(new Camera()), cam2(new Camera()); shared_ptr<Component> composite1(new Composite(fig1, cam1, fig2, cam2, Default())); composite1->operation(); cout << endl; IteratorComponent it = composite1->begin(); composite1->remove(++it); composite1->operation(); cout << endl; shared_ptr<Component> composite2(new Composite(shared_ptr<Component>(new Figure()), composite1, Default())); composite2->operation(); }
Идея
Заместитель (или Proxy) позволяет нам работать не с реальным объектом, а с другим объектом, который подменяет реальный. В каких целях это можно делать?
- Подменяющий объект может контролировать другой объект, задавать правила доступа к этому объекту. Например, у нас есть разные категории пользователей. В зависимости от того, какая у пользователя категория, определять, давать доступ к самому объекту или не давать. Это как защита.
- Так как запросы проходят через заместителя, он может контролировать запросы, заниматься статистической обработкой: считать количество запросов, какие запросы были и так далее.
- Разгрузка объекта с точки зрения запросов. Дело в том, что реальные объекты какие-то операции могут выполнять крайне долго, например, обращение "поиск в базе чего-либо" или "обращение по сети куда-то". Это выполняется долго. Proxy может сохранять предыдущий ответ и при следующем обращении смотреть, был ответ на этот запрос или не был. Если ответ на этот вопрос был, он не обращается к самому хозяину, он заменяет его тем ответом, который был ранее. Естественно, если состояние объекта изменилось, Proxy должен сбросить ту историю, которую он накопил, чтобы выдавать только актуальную информацию.
Это очень удобно, когда у нас тяжелые объекты, операции которых выполняются долго. Зачем еще раз спрашивать, если мы уже спрашивали об этом? Proxy может выдать нам этот ответ.
Диаграмма. Опечатка, должен быть RealSubject
Базовый класс Subject, реальный объект RealObject и объект Proxy, который содержит ссылку на объект, который он замещает. Когда мы работаем через указатель на базовый объект Subject, мы даже не можем понять, с кем мы реально работаем: с непосредственно объектом RealSubject или с его заместителем Proxy. А заместитель может выполнять те задачи, которые мы на него возложили.
Если состояние RealObject изменилось, Прокси должен сбросить историю, которую он накопил
-
Пример кода(Очень много строк, не стоит наверное писать на экзамене)
# include <iostream> # include <memory> # include <map> # include <random> using namespace std; class Subject { public: virtual ~Subject() = 0; virtual pair<bool, double> request(size_t index) = 0; virtual bool changed() { return true; } }; Subject::~Subject() = default; class RealSubject : public Subject { private: bool flag{ false }; size_t counter{ 0 }; public: virtual pair<bool, double> request(size_t index) override; virtual bool changed() override; }; class Proxy : public Subject { protected: shared_ptr<RealSubject> realsubject; public: Proxy(shared_ptr<RealSubject> real) : realsubject(real) {} }; class ConProxy : public Proxy { private: map<size_t, double> cache; public: using Proxy::Proxy; virtual pair<bool, double> request(size_t index) override; }; #pragma region Methods bool RealSubject::changed() { if (counter == 0) { flag = true; } if (++counter == 7) { counter = 0; flag = false; } return flag; } pair<bool, double> RealSubject::request(size_t index) { random_device rd; mt19937 gen(rd()); return pair<bool, double>(true, generate_canonical<double, 10>(gen)); } pair<bool, double> ConProxy::request(size_t index) { pair<bool, double> result; if (!realsubject) { cache.clear(); result = pair<bool, double>(false, 0.); } if (!realsubject->changed()) { cache.clear(); result = realsubject->request(index); cache.insert(map<size_t, double>::value_type(index, result.second)); } else { map<size_t, double>::const_iterator it = cache.find(index); if (it != cache.end()) { result = pair<bool, double>(true, it->second); } else { result = realsubject->request(index); cache.insert(map<size_t, double>::value_type(index, result.second)); } } return result; } #pragma endregion int main() { shared_ptr<RealSubject> subject(new RealSubject()); shared_ptr<Subject> proxy(new ConProxy(subject)); for (size_t i = 0; i < 21; ++i) { cout << "( " << i + 1 << ", " << proxy->request(i % 3).second << " )" << endl; if ((i + 1) % 3 == 0) cout << endl; } }
-
Пример кода(Покороче, стоит писать)
#include <memory> #include <iostream> using namespace std; class BaseSubject { public: BaseSubject() = default; virtual void get_size() = 0; virtual string render() = 0; }; class RealSubject : public BaseSubject { public: RealSubject() { cout << "real subject constructed" << endl; }; virtual void get_size() {}; virtual string render() { cout << "real: render" << endl; return "render"; }; }; class Proxy : public BaseSubject { shared_ptr<RealSubject> rs; shared_ptr<string> r; public: Proxy() { cout << "proxy constructed" << endl; }; Proxy(shared_ptr<RealSubject> rs) : rs(rs) {}; void get_size() { cout << "Proxy: size is 3" << endl; } string render() { if (rs) { if (r) cout << "Proxy: " << *r << endl; else r = make_shared<string>(rs->render()); } else { rs = make_shared<RealSubject>(); r = make_shared<string>(rs->render()); } return *r; } }; int main() { shared_ptr<Proxy> pr = make_shared<Proxy>(); pr->get_size(); pr->render(); pr->render(); }
Плюсы:
- Дает возможность контролировать объект незаметно для клиента
- Может работать тогда, когда объекта нет (пример: ответ от прокси, что объект устаревший)
- Прокси может отвечать за жизненный цикл объекта (может создавать и удалять его)
Минусы:
- Время доступа увеличивается (обработка идет через прокси)
- Прокси должен хранить историю (влияет на память)
Идея
Имеются следующие проблемы:
- У нас большая иерархия. В иерархии возможно несколько внутренних реализаций для объекта, это разные классы, разные ветви. У нас один объект, и во время работы надо поменять реализацию. Каким-то образом нам надо мигрировать от одного класса к другому.
- Постоянно происходит дублирование кода, когда у нас разрастается иерархия классов.
Было предложено разделить понятие самого объекта, его сущности и реализации, в отдельные иерархии. Таким образом, мы сократим количество классов и сделаем систему более гибкой. Мы во время работы сможем менять реализацию. Мы можем уйти (не полностью, частично) от повторного кода.
Паттерн Мост (или Bridge) отделяет саму абстракцию, сущность, от реализаций. Мы можем независимо менять логику (сами абстракции) и наращивать реализацию (добавлять новые классы реализации).
Диаграмма
-
Пример кода. Bridge
# include <iostream> # include <memory> using namespace std; class Implementor { public: virtual ~Implementor() = 0; virtual void operationImp() = 0; }; Implementor::~Implementor() = default; class Abstraction { protected: shared_ptr<Implementor> implementor; public: Abstraction(shared_ptr<Implementor> imp) : implementor(imp) {} virtual ~Abstraction() = 0; virtual void operation() = 0; }; Abstraction::~Abstraction() = default; class ConImplementor : public Implementor { public: virtual void operationImp() override { cout << "Implementor;" << endl; } }; class Entity : public Abstraction { public: using Abstraction::Abstraction; virtual void operation() override { cout << "Entity: "; implementor->operationImp(); } }; int main() { shared_ptr<Implementor> implementor(new ConImplementor()); shared_ptr<Abstraction> abstraction(new Entity(implementor)); abstraction->operation(); }
Таким образом, для каждого объекта мы можем менять реализацию динамически в любой момент во время выполнения. Как логика самого объекта может меняться, так и реализация может меняться. Мы независимо от сущности добавляем реализации и наоборот.
Схема гибкая.
Возникает проблема выноса связи на уровень базового класса. Есть проблема, не всегда это удается сделать.
Но если удается, то это великолепно.
Использование
Используем, когда:
- Нам нужно во время выполнения менять реализацию
- Когда у нас большая иерархия, и по разным ветвям этой иерархии идут одинаковые реализации. Дублирование кода мы выносим в дерево реализаций. Такой подход дает возможность независимо изменять управляющую логику и реализацию.
Идея
У нас есть группа объектов, связанных между собой. Причем эти связи довольно жёсткие. Чтобы извне не работать с каждым объектом в отдельности, мы можем эти все объекты объединить в один класс - фасад, который будет представлять интерфейс для работы со всем этим объединением.
Нам не нужно извне работать с мелкими объектами. Кроме того, фасад может выполнять такую роль, как следить за целостностью нашей системы. Извне, мы, работая с фасадом, работаем, как с простым объектом. Он представляет интерфейс одной сущностью, а внутри у нас целый мир из объектов.
Таким образом:
- упрощается взаимодействие
- уменьшается количество связей за счет объединений фасада.
Это для нас очень важно. При такой организации клиенту не нужно знать и уметь работать с этими объектами. Ему достаточно уметь работать с фасадом.
Диаграмма