-
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();
}
Примечание по 3 лаб-е: В работе есть необходимость в использовании композита, сущности могут быть разными, четко выделяться 3D-модель и поскольку их может много, надо вводить понятие наблюдатель. Можем перемещать какую-то 3D модель и также можем перемещать наблюдатель, при этом наблюдателей может быть несколько, наблюдателя можно связывать с 3D-модель, делать композит => камера и модель. Сейчас мы работаем с каркасными моделями, а вот в курсовой появится такая сущность источник света, может быть несколько и связывать их с моделями, т.е. у них может быть и камера и источник света.
Недостаток:
- Нет того, кто отвечает за работу с ним (держать фокус на композите, принимать решение из чего будет состоять композит).
Декоратор дает возможность нам вынести, то что мы добавляем в компонент. НО возможно такая ситуация, когда производный класс полностью по-своему реализует метод, т.е. он не базируется на базовом, а полностью сам реализует его. Декоратор уже здесь не применим. А что теперь прийти к ситуации разрастании иерархии и дублирования кода? Это плохо!!!
Имеются следующие проблемы:
- У нас большая иерархия. В иерархии возможно несколько внутренних реализаций для объекта, это разные классы, разные ветви. У нас один объект, и во время работы надо поменять реализацию. Каким-то образом нам надо мигрировать от одного класса к другому.
- Постоянно происходит дублирование кода, когда у нас разрастается иерархия классов.
Есть решение использовать паттерн Мост
Идея: отделить сущность от реализации (строить две иерархии сущности и реализации). Таким образом, мы сократим количество классов и сделаем систему более гибкой. Мы во время работы сможем менять реализацию. Мы можем уйти (не полностью, частично) от повторного кода.
Паттерн Мост (или Bridge) отделяет саму абстракцию, сущность, от реализаций. Мы можем независимо менять логику (сами абстракции) и наращивать реализацию (добавлять новые классы реализации).
Возникает проблема выноса связи на уровень базового класса. Есть проблема, не всегда это удается сделать.
Таким образом, для каждого объекта мы можем менять реализацию динамически в любой момент во время выполнения. Как логика самого объекта может меняться, так и реализация может меняться. Мы независимо от сущности добавляем реализации и наоборот.
# include <iostream>
# include <memory>
using namespace std;
class Implementor
{
public:
virtual ~Implementor() = default;
virtual void operationImp() = 0;
};
class Abstraction
{
protected:
shared_ptr<Implementor> implementor;
public:
Abstraction(shared_ptr<Implementor> imp) : implementor(imp) {}
virtual ~Abstraction() = default;
virtual void operation() = 0;
};
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 = make_shared<ConImplementor>();
shared_ptr<Abstraction> abstraction = make_shared<Entity>(implementor);
abstraction->operation();
}
Проблема, дело в том, что некоторые методы могут очень долго выполняться, тяжелые методы точнее, если подумать вот метод выполнился и выдал нам результат, а нам нуден еще раз такой же запрос. Вот к примеру, работа с базой данных, там какой-то запрос поисковый можем выполняться крайне долго. Возникает вопрос, а если еще раз поступил такой же вопрос, зачем еще раз лезть в базу данных, но в базе данных самое главное хранить в оперативной памяти индексные файл для быстрого быстрого доступа, при этом запрос хешируется. Получили ответ, и не хотелось, чтобы не было повторного запроса, т.е. использовать тот ответ, который получили, но на кого возложить ответственность, на тот же объект - это плохо, даже очень.
Для это используем паттерн заместитель.
Идея: Заместитель (или Proxy) позволяет нам работать не с реальным объектом, а с другим объектом, который подменяет реальный. В каких целях это можно делать?
- Подменяющий объект может контролировать другой объект, задавать правила доступа к этому объекту. Например, у нас есть разные категории пользователей. В зависимости от того, какая у пользователя категория категория, определять, давать доступ к самому объекту или не давать. Это как защита.
- Так как запросы проходят через заместителя, он может контролировать запросы, заниматься статистической обработкой: считать количество запросов, какие запросы были и так далее.
- Разгрузка объекта с точки зрения запросов. Дело в том, что реальные объекты какие-то операции могут выполнять крайне долго, например, обращение "поиск в базе чего-либо" или "обращение по сети куда-то". Это выполняется долго. Proxy может сохранять предыдущий ответ и при следующем обращении смотреть, был ответ на этот запрос или не был. Если ответ на этот вопрос был, он не обращается к самому хозяину, он заменяет его тем ответом, который был ранее. Естественно, если состояние объекта изменилось, Proxy должен сбросить ту историю, которую он накопил.
Базовый класс Subject, реальный объект RealObject и объект Proxy, который содержит ссылку на объект, который он замещает. Когда мы работаем через указатель на базовый объект Subject, мы даже не можем понять, с кем мы реально работаем: с непосредственно объектом RealSubject или с его заместителем Proxy. А заместитель может выполнять те задачи, которые мы на него возложили.
Если состояние RealObject изменилось, Прокси должен сбросить историю, которую он накопил.
С помощью Proxy можем фильтровать и анализировать запросы, т.е. говорит что этот запрос ни к чему не приведет хорошему и просто не возвращать.
# include <iostream>
# include <memory>
# include <map>
# include <random>
using namespace std;
class Subject
{
public:
virtual ~Subject() = default;
virtual pair<bool, double> request(size_t index) = 0;
virtual bool changed() { return true; }
};
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 = make_shared<RealSubject>();
shared_ptr<Subject> proxy = make_shared<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;
}
}
К примеру, в лаб. работе 3 у вас много сущностей, сцена, объекты, много посредников, менеджеры, выполняющие разные функции для работы со сценой, надо держать фокус, итератор на наблюдателя, куча всего. И очень плохо работать со всем эти с вне!!!
Идея: взаимодействовать со всеми классами как с целостной оболочкой. Скрывать сложный мир.
У нас есть группа объектов, связанных между собой. Причем эти связи довольно жёсткие. Чтобы извне не работать с каждым объектом в отдельности, мы можем эти все объекты объединить в один класс - фасад, который будет представлять интерфейс для работы со всем этим объединением.
Нам не нужно извне работать с мелкими объектами. Кроме того, фасад может выполнять такую роль, как следить за целостностью нашей системы. Извне, мы, работая с фасадом, работаем, как с простым объектом. Он представляет интерфейс одной сущностью, а внутри у нас целый мир мир из объектов. Таким образом, упрощается взаимодействие, уменьшается количество связей за счет объединений фасада.
Это для нас очень важно. При такой организации клиенту не нужно знать и уметь работать с этими объектами. Ему достаточно уметь работать с фасадом.