Skip to content

ООП 15. Паттерны поведения: посетитель (Visitor), опекун (Memento), шаблонный метод (Template Method), хранитель (Holder), итератор (Iterator), свойство (Property). Их преимущества и недостатки.

Kozlitin Maxim edited this page Jun 14, 2023 · 2 revisions

Преимущества использования паттернов:

  • Мы имеем готовое решение
  • За счет готового решения - нюансы все выявлены => надежный код
  • Повышается скорость разработки
  • Повышается читаемость кода
  • Улучшается взаимодействие с коллегами (достаточно сказать название паттерна, который вы используете, и всё сразу станет понятно)

Посетитель

Идея

  • Следующая проблема - связанная с изменением интерфейса объектов. Если мы используем полиморфизм, мы не можем в производном кассе ни сузить, ни расширить интерфейс, так как он должен четко поддерживать интерфейс базового класса.

  • Если нам необходимо расширить интерфейс, можно использовать паттерн Визитёр. Он позволяет во время выполнения (в отличие от паттерна Адаптера, который решает эту проблему до выполнения) подменить или расширить функционал.

  • Диаграмма

    Чтобы можно было поменять/расширять функционал, а базовом классе добавляем метод accept(Visitor). Соответственно, все производные классы могут подменять этот метод.

image-6

  • Пример кода. 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 - снимок объекта в какой-то момент времени. Опекун отвечает за хранение этих снимков и возможность вернуться к предыдущему состоянию объекта

image-7

  • Пример кода. Опекун (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;
    }

Преимущества

Позволяет не грузить сам класс задачей сохранять предыдущие состояния.

Недостатки

Опекуном надо управлять. Он наделал снимков, а они нам не нужны. Кто-то должен их очищать. Какой механизм очистки? Много памяти тратится.

Над этим всем должен стоять кто-то, который решает, когда, например делать снимки

Шаблонный метод

Идея

Этот паттерн является скелетом какого-либо метода. Мы любую задачу разбиваем на этапы - формируем шаги, которые мы выполняем для того, чтобы то, что мы получили на входе, преобразовать в результат, который нам нужен.

Есть диаграмма, которая по существу задает нам методы.

  • Диаграмма

image

А вот реальная диаграмма:

image-2

  • Пример кода. Шаблонный метод (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();
    }

Хранитель(Holder)

Существует проблема. Предположим, у нас есть класс А, в котором есть метод 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. Этот объект может быть удалён, и в этом случае возникает проблема: указатель, инициализированный каким-то адресом, будет указывать на удалённый объект. Можно рассматривать каждый объект, который держит указатель, как хранитель. То есть мы отдаём указатель на объект, а объект-хранитель считает, что этот объект его собственный, происходит захват.

    В случае если хранитель отдаёт объект, нужно позаботиться о том, чтобы не образовался "висящий" указатель, то есть указатель на объект, которого нет.

    Проблема с утечкой памяти не такая острая как проблема с висящим указателем. Утечка памяти приводит всего лишь к нехватке памяти, в то время как с висящим указателем мы можем случайно вызвать метод несуществующего объекта, что приведёт к падению системы.

    Представим, что на один объект держат указатели несколько объектов. Как понять, какой из объектов должен удалять этот указатель? Если это отдавать на откуп программиста, то о надежности такого кода говорить нельзя, возможно ошибка. Допустим, мы выбрали один из объектов ответственным. Какая гарантия, что он не уничтожится раньше, чем другие два объекта?

Итератор (Iterator)

Предоставляет способ доступа к элементам контейнера, независимо от его внутреннего устройства. в с++ введён новый цикл 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;
    }

Паттерн Свойство(Property)

Не столько паттерн поведения, сколько шаблон, который нам даёт возможность формализовать свойства. В современных языках есть понятие свойство. Предположим, у нас есть какой то класс, и у его объекта есть свойство 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;
    }
Clone this wiki locally