-
Notifications
You must be signed in to change notification settings - Fork 0
ООП 15. Паттерны поведения: посетитель (Visitor), опекун (Memento), шаблонный метод (Template Method), хранитель (Holder), итератор (Iterator), свойство (Property). Их преимущества и недостатки.
- Мы имеем готовое решение
- За счет готового решения - нюансы все выявлены => надежный код
- Повышается скорость разработки
- Повышается читаемость кода
- Улучшается взаимодействие с коллегами (достаточно сказать название паттерна, который вы используете, и всё сразу станет понятно)
Идея
-
Следующая проблема - связанная с изменением интерфейса объектов. Если мы используем полиморфизм, мы не можем в производном кассе ни сузить, ни расширить интерфейс, так как он должен четко поддерживать интерфейс базового класса.
-
Если нам необходимо расширить интерфейс, можно использовать паттерн Визитёр. Он позволяет во время выполнения (в отличие от паттерна Адаптера, который решает эту проблему до выполнения) подменить или расширить функционал.
-
Диаграмма
Чтобы можно было поменять/расширять функционал, а базовом классе добавляем метод accept(Visitor). Соответственно, все производные классы могут подменять этот метод.
-
Пример кода. Visitor
#include <iostream> #include <memory> #include <vector> using namespace std; class Circle; class Rectangle; class Visitor { public: virtual ~Visitor() = default; virtual void visit(Circle& ref) = 0; virtual void visit(Rectangle& ref) = 0; }; class Shape { public: virtual ~Shape() = default; virtual void accept(shared_ptr<Visitor> visitor) = 0; }; class Circle : public Shape { public: virtual void accept(shared_ptr<Visitor> visitor) override { visitor->visit(*this); } }; class Rectangle : public Shape { public: virtual void accept(shared_ptr<Visitor> visitor) override { visitor->visit(*this); } }; class ConVisitor : public Visitor { public: virtual void visit(Circle& ref) override { cout << "Circle;" << endl; } virtual void visit(Rectangle& ref) override { cout << "Rectangle;" << endl; } }; class Formation { public: static vector<shared_ptr<Shape>> initialization( initializer_list<shared_ptr<Shape>> list) { vector<shared_ptr<Shape>> vec; for (auto elem : list) vec.push_back(elem); return vec; } }; int main() { vector<shared_ptr<Shape>> figure = Formation::initialization( {make_shared<Circle>(), make_shared<Rectangle>(), make_shared<Circle>()}); shared_ptr<Visitor> visitor = make_shared<ConVisitor>(); for (auto& elem : figure) elem->accept(visitor); }
Визитёр один функционал собирает в одно место для разных классов. Для каждого такого класса/подкласса есть свой метод, который принимает элемент этого подкласса. Конкретный визитёр уже реализует этот функционал.
Преимущества
- В один класс сводим методы, относящиеся к одному функционалу
Недостатки
- Расширяется иерархия, добавляются новые классы. Проблема связи на уровне базовых классов
- Может меняться иерархия (крайне редкая проблема). Тогда визитёр не срабатывает.
- Есть проблема как у стратегии - проблема с данными, данные могут меняться
Тассов говорил что вопросы с иерархией можно решить с помощью темплейтного визитера и паттерна CRTP с переменным числом параметров, ну это так, пиздануть, чтобы ему понравилось (14_3 23:33)
Этот паттерн имеет много названий. Одно из них - Опекун.
Идея
Когда мы выполняем много операций, объект изменяется, но, те изменения которые у нас произошли, могут нас не устраивать, и мы можем вернуться к предыдущему состоянию нашего объекта (откат).
Как вариант - хранить те, которые были у объекта. Если возложить эту задачу на объект - он получится тяжелым.
Идея - выделить эту обязанность другому объекту, задача которого - хранить предыдущие состояния нашего объекта, который, если нужно, позволит нам вернуться к какому-либо предыдущему состоянию.
-
Диаграмма(set Object)
Memento - снимок объекта в какой-то момент времени. Опекун отвечает за хранение этих снимков и возможность вернуться к предыдущему состоянию объекта
-
Пример кода. Опекун (Memento).
# include <iostream> # include <memory> # include <list> using namespace std; class Memento; class Caretaker { public: unique_ptr<Memento> getMemento(); void setMemento(unique_ptr<Memento> memento); private: list<unique_ptr<Memento>> mementos; }; class Originator { public: Originator(int s) : state(s) {} const int getState() const { return state; } void setState(int s) { state = s; } std::unique_ptr<Memento> createMemento() { return make_unique<Memento>(*this); } void restoreMemento(std::unique_ptr<Memento> memento); private: int state; }; class Memento { friend class Originator; public: Memento(Originator o) : originator(o) {} private: void setOriginator(Originator o) { originator = o; } Originator getOriginator() { return originator; } private: Originator originator; }; #pragma region Methods Caretaker void Caretaker::setMemento(unique_ptr<Memento> memento) { mementos.push_back(move(memento)); } unique_ptr<Memento> Caretaker::getMemento() { unique_ptr<Memento> last = move(mementos.back()); mementos.pop_back(); return last; } #pragma endregion #pragma region Method Originator void Originator::restoreMemento(std::unique_ptr<Memento> memento) { *this = memento->getOriginator(); } #pragma endregion int main() { auto originator = make_unique<Originator>(1); auto caretaker = make_unique<Caretaker>(); cout << "State = " << originator->getState() << endl; caretaker->setMemento(originator->createMemento()); originator->setState(2); cout << "State = " << originator->getState() << endl; caretaker->setMemento(originator->createMemento()); originator->setState(3); cout << "State = " << originator->getState() << endl; caretaker->setMemento(originator->createMemento()); originator->restoreMemento(caretaker->getMemento()); cout << "State = " << originator->getState() << endl; originator->restoreMemento(caretaker->getMemento()); cout << "State = " << originator->getState() << std::endl; originator->restoreMemento(caretaker->getMemento()); cout << "State = " << originator->getState() << std::endl; }
Преимущества
Позволяет не грузить сам класс задачей сохранять предыдущие состояния.
Недостатки
Опекуном надо управлять. Он наделал снимков, а они нам не нужны. Кто-то должен их очищать. Какой механизм очистки? Много памяти тратится.
Над этим всем должен стоять кто-то, который решает, когда, например делать снимки
Идея
Этот паттерн является скелетом какого-либо метода. Мы любую задачу разбиваем на этапы - формируем шаги, которые мы выполняем для того, чтобы то, что мы получили на входе, преобразовать в результат, который нам нужен.
Есть диаграмма, которая по существу задает нам методы.
- Диаграмма
А вот реальная диаграмма:
-
Пример кода. Шаблонный метод (Template Method).
# include <iostream> using namespace std; class AbstractClass { public: void templateMethod() { primitiveOperation(); concreteOperation(); hook(); } protected: virtual void primitiveOperation() = 0; void concreteOperation() { cout << "concreteOperation;" << endl; } virtual void hook() { cout << "hook Base;" << endl; } }; class ConClassA : public AbstractClass { protected: virtual void primitiveOperation() override { cout << "primitiveOperation A;" << endl; } }; class ConClassB : public AbstractClass { protected: virtual void primitiveOperation() override { cout << "primitiveOperation B;" << endl; } void hook() { cout << "hook B;" << endl; } }; int main() { ConClassA ca; ConClassB cb; ca.templateMethod(); cb.templateMethod(); }
Существует проблема. Предположим, у нас есть класс А, в котором есть метод f()
. Страшный код. Мы не знаем, что творится внутри f()
, и, естественно, мы используем механизм обработки исключительных ситуаций. Внутри f()
происходит исключительная ситуация, она приводит к тому, что мы перескакиваем на какой-то обработчик, неизвестно где находящийся. Это приводит к тому, что объект p
не удаляется - происходит утечка памяти.
-
Страшный код:
{ A* p = new A; p->f(); // Внутри f() происходит исключительная ситуация delete p; // Объект p не удаляется }
Как решить эту проблему?
Мы можем указатель p
обернуть в какой-то объект - хранитель. Этот объект будет содержать указатель на объект A
. Задача объекта: при выходе из области видимости объекта-хранителя будет вызываться деструктор obj
, в котором мы можем уничтожить объект A
.
{
Holder<A> obj(new A);
}
Для объекта хранителя достаточно определить три операции - *
(получить значение по указателю), ->
(обратиться к методу объекта, на который указывает указатель) и bool
(проверить, указатель указывает на объект, nullptr
он или нет). Чтобы можно было записать obj->f();
. То есть эта оболочка должна быть "прозрачной". Её задача должна быть только вовремя освободить память, выделенную под объект. Мы работаем с объектом класса А через эту оболочку.
-
Пример кода. Хранитель(Holder)
template <typename T> class Holder { T* ptr{nullptr}; // Указатель на объект (сразу же его обнуляем) public: explicit Holder(T *p) : ptr(p) {}; // Захватываем указатель и запрещаем неявный вызов конструктора ~Holder() {delete ptr;} // Задача деструктора - удалить объект // Определяем джентельменский набор из трёх операторов T& operator *() const {return *ptr;} T* operator ->() const {return ptr;} operator bool() const { return ptr != nullptr; } // Запрещаем конструктор копирования и оператор присваивания Holder(const Holder &) = delete; // Если у нас параметр T по умолчанию, можно его явно не указвать // а использовать & (просто пояснение) Holder operator=(const Holder &) = delete; };
-
Проблема висящего указателя
Этот хранитель решает ситуацию, связанную с обработкой исключительных ситуаций. Но предположим, что у нас есть один объект класса
A
и классB
держит указатель на объект классаA
.class A {...}; class B { A* p; }
Например, мы получили указатель
p
. Этот объект может быть удалён, и в этом случае возникает проблема: указатель, инициализированный каким-то адресом, будет указывать на удалённый объект. Можно рассматривать каждый объект, который держит указатель, как хранитель. То есть мы отдаём указатель на объект, а объект-хранитель считает, что этот объект его собственный, происходит захват.В случае если хранитель отдаёт объект, нужно позаботиться о том, чтобы не образовался "висящий" указатель, то есть указатель на объект, которого нет.
Проблема с утечкой памяти не такая острая как проблема с висящим указателем. Утечка памяти приводит всего лишь к нехватке памяти, в то время как с висящим указателем мы можем случайно вызвать метод несуществующего объекта, что приведёт к падению системы.
Представим, что на один объект держат указатели несколько объектов. Как понять, какой из объектов должен удалять этот указатель? Если это отдавать на откуп программиста, то о надежности такого кода говорить нельзя, возможно ошибка. Допустим, мы выбрали один из объектов ответственным. Какая гарантия, что он не уничтожится раньше, чем другие два объекта?
Предоставляет способ доступа к элементам контейнера, независимо от его внутреннего устройства. в с++ введён новый цикл for each:
-
Пример разложения
// Это: for (auto elem : obj) cout << elem; // Разложится в это: for (Iterator <Type> It = obj.begin(); It != obj.end(); ++It) { auto elem = *It; cout << elem; }
-
Пример кода. Iterator. (без проверок и обработки исключительных ситуаций)
# include <iostream> # include <memory> # include <iterator> # include <initializer_list> using namespace std; template <typename Type> class Iterator; class BaseArray { public: BaseArray(size_t sz = 0) { count = shared_ptr<size_t>( new size_t(sz) ); } virtual ~BaseArray() = default; size_t size() { return bool(count) ? *count : 0; } operator bool() { return size(); } protected: shared_ptr<size_t> count; }; template <typename Type> class Array final : public BaseArray { public: Array(initializer_list<Type> lt); virtual ~Array() {} Iterator<Type> begin() const { return Iterator<Type>(arr, count); } Iterator<Type> end() const { return Iterator<Type>(arr, count, *count); } private: shared_ptr<Type[]> arr{ nullptr }; }; template <typename Type> class Iterator : public std::iterator<std::input_iterator_tag, Type> { friend class Array<Type>; private: Iterator(const shared_ptr<Type[]>& a, const shared_ptr<size_t>& c, size_t ind = 0) : arr(a), count(c), index(ind) {} public: Iterator(const Iterator &it) = default; bool operator!=(Iterator const& other) const; bool operator==(Iterator const& other) const; Type& operator*(); const Type& operator*() const; Type* operator->(); const Type* operator->() const; Iterator<Type>& operator++(); Iterator<Type> operator++(int); private: weak_ptr<Type[]> arr; weak_ptr<size_t> count; size_t index = 0; }; #pragma region Method Array template <typename Type> Array<Type>::Array(initializer_list<Type> lt) { if (!(*count = lt.size())) return; arr = shared_ptr<Type[]>(new Type[*count]); size_t i = 0; for (Type elem : lt) arr[i++] = elem; } #pragma endregion #pragma region Methods Iterator template <typename Type> bool Iterator<Type>::operator!=(Iterator const& other) const { return index != other.index; } template <typename Type> Type& Iterator<Type>::operator*() { // нужно проверить корректность итератора shared_ptr<Type[]> a(arr); return a[index]; } template <typename Type> Iterator<Type>& Iterator<Type>::operator++() { shared_ptr<size_t> n(count); if (index < *n) index++; return *this; } template <typename Type> Iterator<Type> Iterator<Type>::operator++(int) { Iterator<Type> it(*this); ++(*this); return it; } #pragma endregion template <typename Type> ostream& operator<<(ostream& os, const Array<Type>& arr) { for (auto elem : arr) cout << elem << " "; return os; } int main() { Array<int> arr{ 1, 2, 3, 4, 5 }; cout << " Array: " << arr << endl; }
Не столько паттерн поведения, сколько шаблон, который нам даёт возможность формализовать свойства. В современных языках есть понятие свойство. Предположим, у нас есть какой то класс, и у его объекта есть свойство V.
A objl
obj V = 2;
int i = obj.V;
(В реальном мире такого доступа быть не должно). Доступ осуществляется через методы, один устанавливает - set(), другой возвращает - get(). Если мы будем рассматривать V как открытый член, но не простое данное... Если рассматривать V как объект, мы для него должны определить V как оператор присваивания.
Нам нужен класс, в котором есть перегруженный оператор = и оператор приведения типа. Этот перегруженный оператор равно должен вызывать метод установки сет, а этот гет.
Удобно создать шаблон свойства. Первый параметр - тип объекта для которого создается шаблон, а второй параметр - тип объекта к которому приводится и ли инициализируется значение. В данном случае это целый тип. Getter - метод класса который возвращает Type, а Setter - установка для вот этого объекта.
-
Пример кода. Свойство(Property)
# include <iostream> # include <memory> using namespace std; template <typename Owner, typename Type> class Property { private: using Getter = Type (Owner::*)() const; using Setter = void (Owner::*)(const Type&); Owner* owner; Getter methodGet; Setter methodSet; public: Property() = default; Property(Owner* owr, Getter getmethod, Setter setmethod) : owner(owr), methodGet(getmethod), methodSet(setmethod) {} void init(Owner* owr, Getter getmethod, Setter setmethod) { owner = owr; methodGet = getmethod; methodSet = setmethod; } operator Type() { return (owner->*methodGet)(); } // Getter void operator=(const Type& data) { (owner->*methodSet)(data); } // Setter // Property(const Property&) = delete; // Property& operator=(const Property&) = delete; }; class Object { private: double value; public: Object(double v) : value(v) { Value.init(this, &Object::getValue, &Object::setValue); } double getValue() const { return value; } void setValue(const double& v) { value = v; } Property<Object, double> Value; }; int main() { Object obj(5.); cout << "value = " << obj.Value << endl; obj.Value = 10.; cout << "value = " << obj.Value << endl; unique_ptr<Object> ptr(new Object(15.)); cout << "value =" << ptr->Value << endl; obj = *ptr; obj.Value = ptr->Value; }