-
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;
}Рассмотрим, например ваша лаб. работа 3, выше в декораторе рассматривали один компонент, а если компонентов несколько, может быть не одна фигура, а несколько. Нам надо над всеми фигура выполнить одно и тоже действие. Пусть к примеру, у нас сместился наблюдатель, то по отношению к наблюдатели все объекты должны сместиться.
Можно взять контейнер(вектор, список), обернуть эти компоненты в контейнер и обходит его, используя итератор(или нет) циклом. ООП же у нас программирования без выбора, но и циклы тоже это плохо, циклы можно использовать, но их надо постараться спрятать или же обойтись без них. Работа во вне только со сущностью.
Идея: Создать класс, который будет производным от базового компонента, т.е. будем работать как с компонентом, но здесь в себя будет включать список фигур, т.е. спрячем этот список в композит. => Вынести интерфейс, который предлагает контейнер (объект, включающий в себя другие объекты), на уровень базового класса.
Композит - это обёртка над списком, по существу это тоже самое что и контейнер, должны представить интерфейс работы с содержимым самого контейнера (добавлять, удалять, держать фокус для компонента).А если этого нет в базовом, то нужно добавить этот интерфейс, т.е. представить интерфейс, которые должны поддерживать производные => добавить в базовые методы удаления, добавления, держать фокуса для компонента и Итератор, при этом реализация идет этих методов на уровне базового класса.
Задача методов интерфейса для компонентов - пройтись по списку компонент и выполнить соответствующий метод.
Компоновщик компонует объекты в древовидную структуру, в которой над всей иерархией объектов можно выполнять такие же действия, как над простым элементом или над его частью.
Каждый композит может включать в себя как простые компоненты, так и другие композиты. Получается древовидная структура. Системе неважно, композит это или не композит. Она работает с любым элементом.

- Базовый класс - Component. Нам должно быть безразлично, с каким объектом мы работаем: то ли это один компонент, то ли это объект, включающий в себя другие объекты (контейнер). Если это контейнер, то нам надо работать с содержимым контейнера, удалять, добавлять в него объекты. Идея - вынести этот интерфейс на уровень базового класса (добавление компонента - add(Component), удаление компонента - remove(Iterator), createIterator()). Нам надо четко понимать, когда мы работаем с каким-то компонентом, чем он является: один объект или контейнер. Для этого нам нужен метод isComposite(). То, что мы можем делать с Component - operation() - чисто виртуальные методы. Остальные (add, remove, и т. д.) мы будем реализовывать.
- ConComponent - простой компонент, его задачей будет только реализовать остальные методы - operation.
- Composite - составной класс. Реализует все те методы, что есть в компоненте. Он содержит в себе список компонент.
# include <iostream>
# include <initializer_list>
# include <memory>
# include <vector>
# include <iterator>
using namespace std;
class Component;
using PtrComponent = shared_ptr<Component>;
using VectorComponent = vector<PtrComponent>;
using IteratorComponent = VectorComponent::const_iterator;
class Component
{
public:
virtual ~Component() = default;
virtual void operation() = 0;
virtual bool isComposite() const { return false; }
virtual bool add(initializer_list<PtrComponent> comp) { return false; }
virtual bool remove(const IteratorComponent& it) { return false; }
virtual IteratorComponent begin() const { return IteratorComponent(); }
virtual IteratorComponent end() const { return IteratorComponent(); }
};
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(initializer_list<PtrComponent> list) override;
virtual bool remove(const IteratorComponent& it) override { vec.erase(it); return true; }
virtual IteratorComponent begin() const override { return vec.begin(); }
virtual IteratorComponent end() const override { return vec.end(); }
};
#pragma region Methods
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();
}
bool Composite::add(initializer_list<PtrComponent> list)
{
for(auto elem : list)
vec.push_back(elem);
return true;
}
#pragma endregion
int main()
{
using Default = shared_ptr<Component>;
PtrComponent fig = make_shared<Figure>(), cam = make_shared<Camera>();
auto composite1 = make_shared<Composite>(fig, cam, Default());
composite1->add({ make_shared<Figure>(), make_shared<Camera>() });
composite1->operation();
cout << endl;
IteratorComponent it = composite1->begin();
composite1->remove(++it);
composite1->operation();
cout << endl;
auto composite2 = make_shared<Composite>(make_shared<Figure>(), composite1, Default());
composite2->operation();
}Их очень много, рассмотрим только основную часть