-
Notifications
You must be signed in to change notification settings - Fork 2
ООП Лекция 13. Структурные паттерны.
Структурные паттерны предлагают структурные решения. Это какая-либо декомпозиция классов с использованием схем наследования, включения...
Если одна сущность выполняет несколько ответственностей, надо разделять задачи => проблема дальнейшего развития. В случае если какая-то ответственность меняется, а другая нет, что делать? Надо разделять как в фабричном методе, любой порождающий паттерн надо выделять ответственного за принятия решений. Как быть?
Проблема, с которой мы сталкиваемся - это когда нам надо создавать объекты, и эти объекты в разных местах программы мы используем по-разному. Грубо говоря, один объект выступает в нескольких ролях. Из этого следует:
- Для каждой роли используется свой интерфейс. В принципе, эти интерфейсы могу пересекаться. Но, если мы будем создавать такой класс, мы получим избыточный интерфейс.
- Как правило, эти роли завязаны еще под ответственность. Один и тот же объект будет иметь несколько ответственностей, что с точки зрения ООП очень плохо. Каждый объект должен иметь только одну ответственностей.
Идея: У нас один и тот же объект выполняет несколько ролей, физически это один объект. В разных местах мы работаем с ним по-разному, т.е. разные интерфейсы. Выделить простой класс, а посредники будет представлять нужный класс для работы.
- Несколько ответственностей плохо, невозможно дальнейшее развитие ответственностей
- Объект выполняет несколько ответственностей, то одну ответственность выносим на адаптер, то есть за одну ответственность отвечает сам объект, тем самым не нарушаем принцип ООП, за другую же адаптер.
- используем Адаптер, когда нужно подменять сущность, то есть если у нас был программа из каких-то библиотек, подсистем или что-то свое, но нам требуется другой интерфейс как раз чтобы использовать другой класс, то очень удобен адаптер для внедрения через него.
- Должны использовать обобщение - базовое понятие, класс абстрактный должен нам предоставлять интерфейс, при этом мы не должны его расширять или сужать.
Реализуем понятие или сущность какую-то с простым интерфейсом, а любая работа через посредника идет, вот из диаграммы можно увидеть, что:
-
ConAdapterA
- это одна роль объекта -
ConAdapterB
- это другая роль того же самого объекта
и при изменении какой из них, другая не меняется. В разных частях программы по-разному работаем с эти объектом.
Кроме это задачи с разделением ролей он решает еще задачи, есть система используется какую библиотеку, но хотите поменять что-то, но здесь другой интерфейс, и чтобы не переписывать код применяется адаптер.
Примечание: Solution подобный => вырожденный Адаптер
Преимущества:
- Он позволяет нам класс с любым интерфейсом использовать в нашей программе.
- Позволяет не создавать нам классы с несколькими ответственностями. Разносим это по другим классам.
Недостатки:
- Просто не понять без UML.
- Дублирование кода (Кучу классов), нет мертвых классов.
# include <iostream>
# include <memory>
using namespace std;
class Adapter
{
public:
virtual ~Adapter() = default;
virtual void request() = 0;
};
class BaseAdaptee
{
public:
virtual ~BaseAdaptee() = default;
virtual void specificRequest() = 0;
};
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 = make_shared<ConAdaptee>();
shared_ptr<Adapter> adapter = make_shared<ConAdapter>(adaptee);
adapter->request();
}
Пусть у нас есть какое-то абстрактное понятие, draw()
и есть какая-то реализация, начинаем развивать, т.е. от класса получаем производные чтобы подменить одно на другое. По идем нам надо нарисовать, что и рисуют выше стоящие классы и добавить что-то свое.
void C::draw()
{
B::draw();
_draw();
}
Проблемы:
- Дублирование кода при добавлении одного и того же. (Речь о конкретных компонентов).
- Большая иерархия классов, в случае что смешивать в конкретных компонентах
Идея: Добавление выделить в отдельный класс. - самый первый пример с
draw()
,_draw()
выделить отдельно.
Задача Декоратора: дополнить функционал интерфейса сущности, которые существуют, чтобы избежать дублирования кода.
Есть класс Component. От него производные классы - какие-то конкретные компоненты: ComponentA
и ComponentB
. Мы создаем класс Decorator, который обладает таким же интерфейсом, но, кроме всего прочего, он имеет ссылку на базовый класс Component. От него мы уже можем порождать конкретные декораторы - Decorator1
и Decorator2
. Decorator имеет ссылку на компонент, и вызывая метод компонента, на который держится ссылка, добавляет свой функционал. Таким образом, мы можем добавить к любому компоненту единый функционал.
Таким образом, мы можем отддекорировать сам декоратор...
Преимущества:
- Мы получаем крайне гибкую систему, избавляясь от колоссального количества классов. Резко уменьшается иерархия.
- Декорировать можем во время выполнения работы программы.
- Избавляемся от дублирования кода. Этот код уходит в конкретный декоратор, не дублируясь.
Примечание: Во время выполнения программы мы можем просто продекорировать. У нас возможно изменение поведения объекта во время выполнения программы.
Недостатки:
- Долгий вызов виртуальных методов в декораторе
- Проблемы вычленения декораторов при наличии огромного количества обёрток
- Нет контроля со стороны компилятора над обёртками (кто-то должен за них отвечать). Примечание: Когда мы сделали сложную обертку, нам, к сожалению, убрать какую-то обёртку будет проблематично. Нам придется заново создавать компонент с обёртками. Мы не можем её вычеркнуть.
# include <iostream>
# include <memory>
using namespace std;
class Component
{
public:
virtual ~Component() = default;
virtual void operation() = 0;
};
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; ";
}
}
#pragma endregion
int main()
{
shared_ptr<Component> component = make_shared<ConComponent>();
shared_ptr<Component> decorator1 = make_shared<ConDecorator>(component);
decorator1->operation();
cout << endl;
shared_ptr<Component> decorator2 = make_shared<ConDecorator>(decorator1);
decorator2->operation();
cout << endl;
}
Их очень много, рассмотрим только основную часть