-
Notifications
You must be signed in to change notification settings - Fork 0
ООП 13. Порождающие паттерны: одиночка (Singleton), прототип (Prototype), пул объектов (Object Pool). Их преимущества и недостатки.
Отличие шаблонов от паттернов: шаблон - конкретная реализация чего-либо, а паттерн - шаблон для решения какой-то задачи (как может решаться данная задача). Паттерн мы всегда адаптируем к своей задаче.
- Мы имеем готовое решение
- За счет готового решения - нюансы все выявлены => надежный код
- Повышается скорость разработки
- Повышается читаемость кода
- Улучшается взаимодействие с коллегами (достаточно сказать название паттерна, который вы используете, и всё сразу станет понятно)
Задача порождающего паттерна: создание объектов
Проблема: не может быть полиморфных конструкторов. Если внутри метода мы работаем со ссылкой/указателем на базовое понятие, будет вызван конструктор базового понятия.
Возникают задачи, в которых должно быть гарантировано, что создан только один объект класса.
Решение: убрать конструкторы из public части. В паблик части сделать статический метод, который будет при необходимости порождать объект. В статическом методе содержится статический член класса своего объекта. Поскольку он статический – будет создан только один раз. Конструктор находится в private части. Нельзя копировать – запрещаем конструктор копирования и оператор присваивания.
Недостатки метода:
- Глобальный объект: доступ через глобальный интерфейс, вызовом статического метода
- Проблема шаблона: лишаемся подмены. Решение о том, какой объект создавать, принимается на этапе компиляции. Шаблоны здесь лучше не использовать.
Альтернатива:
-
фабричный метод.
-
Пример кода. Singleton обычный (Запрещаем конструктор копирования и оператор присваивания)
# include <iostream> # include <memory> using namespace std; class Product { public: static shared_ptr<Product> instance() { static shared_ptr<Product> myInstance(new Product()); return myInstance; } ~Product() { cout << "Destructor;" << endl; } void f() { cout << "Method f;" << endl; } Product(const Product&) = delete; // запрещаем Product& operator=(const Product&) = delete; // запрещаем private: Product() { cout << "Default constructor;" << endl; } }; int main() { shared_ptr<Product> ptr(Product::instance()); ptr->f(); }
-
Пример кода. Singleton шаблонный (Запрещаем конструктор копирования и оператор присваивания)
# include <iostream> # include <memory> using namespace std; template < Type> class Singleton { public: static Type& instance() { static unique_ptr<Type> myInstance(new Type()); return *myInstance; } Singleton() = delete; Singleton(const Singleton<Type>&) = delete; Singleton<Type>& operator=(const Singleton<Type>&) = delete; }; class Product { public: Product() { cout << "Default constructor;" << endl; } ~Product() { cout << "Destructor;" << endl; } void f() { cout << "Method f;" << endl; } }; int main() { Product& d = Singleton<Product>::instance(); d.f(); }
- Проблема: Хотим создать копию объекта, не зная его класса. (Например, в подменяемом методе). Также мы не хотим тащить за ним его Creator’ы.
- Решение: Добавляем в базовый класс метод clone(), который создаст новый объект на основе существующего. Производные классы реализуют clone() под себя.
-
Пример кода. Prototype
# include <iostream> # include <memory> using namespace std; class BaseObject { public: virtual ~BaseObject() = default; virtual unique_ptr<BaseObject> clone() = 0; }; class Object1 : public BaseObject { public: Object1() { cout << "Default constructor;" << endl; } Object1(const Object1& obj) { cout << "Copy constructor;" << endl; } ~Object1() { cout << "Destructor;" << endl; } virtual unique_ptr<BaseObject> clone() override { return unique_ptr<BaseObject>(new Object1(*this)); } }; int main() { unique_ptr<BaseObject> ptr1(new Object1()); auto ptr2 = ptr1->clone(); }
Преимущества: очень удобно использовать, когда объект сложно создаётся.
Пул объектов предоставляет набор готовых объектов, которые мы можем использовать
Пример использования
Многопроцессорная система. Количество заданий, которое мы должны создать, должно быть не больше количества процессоров. Чтобы каждый процессор выполнял свою задачу.
Когда надо использовать?
Когда создание или уничтожение какого-либо объекта - трудоемкий процесс и надо "держать" определенное количество объектов в системе.
Задачи
- Он держит эти объекты.
- Может их создавать (то есть может расширяться).
- По запросу отдает объект.
- Если клиенту этот объект не нужен, он может его вернуть в пул.
Исходя из пунктов 3 и 4, для каждого включенного в пул объекта мы должны установить, используется он или не используется.
Возможна утечка информации. Мы взяли объект из пула, поработали, вернули в пул в том состоянии, в каком оставили – его теперь надо или вернуть в исходное состояние, или очистить, чтобы следующему клиенту не попала информация прошлого.
Пул объектов можно с помощью одиночки – чтобы пул объектов был только один. Пул объектов – контейнерный класс, для него используем итератор. Необходимо знать, занят объект или нет, используем пару: ключ (bool) и объект.
- Диаграмма
-
Пример кода. ObjectPool
#include <iostream> #include <iterator> #include <memory> #include <vector> using namespace std; class Product { private: static size_t count; public: Product() { cout << "Constructor(" << ++count << ");" << endl; } ~Product() { cout << "Destructor(" << count-- << ");" << endl; } void clear() { cout << "Method clear: 0x" << this << endl; } }; size_t Product::count = 0; template <typename Type> class ObjectPool { public: static shared_ptr<ObjectPool<Type>> instance(); // статический метод из одиночки shared_ptr<Type> getObject(); bool releaseObject(shared_ptr<Type>& obj); size_t count() const { return pool.size(); } iterator<output_iterator_tag, const pair<bool, shared_ptr<Type>>> begin() const; iterator<output_iterator_tag, const pair<bool, shared_ptr<Type>>> end() const; ObjectPool(const ObjectPool<Type>&) = delete; ObjectPool<Type>& operator=(const ObjectPool<Type>&) = delete; private: vector<pair<bool, shared_ptr<Type>>> pool; ObjectPool() {} pair<bool, shared_ptr<Type>> create(); template <typename Type> friend ostream& operator<<(ostream& os, const ObjectPool<Type>& pl); }; template <typename Type> shared_ptr<ObjectPool<Type>> ObjectPool<Type>::instance() { static shared_ptr<ObjectPool<Type>> myInstance(new ObjectPool<Type>()); return myInstance; } template <typename Type> shared_ptr<Type> ObjectPool<Type>::getObject() { size_t i; for (i = 0; i < pool.size() && pool[i].first; ++i) ; if (i < pool.size()) { pool[i].first = true; } else { pool.push_back(create()); } return pool[i].second; } template <typename Type> bool ObjectPool<Type>::releaseObject(shared_ptr<Type>& obj) { size_t i; for (i = 0; i < pool.size() && pool[i].second != obj; ++i) ; if (i == pool.size()) return false; obj.reset(); pool[i].first = false; pool[i].second->clear(); return true; } template <typename Type> pair<bool, shared_ptr<Type>> ObjectPool<Type>::create() { return pair<bool, shared_ptr<Type>>(true, shared_ptr<Type>(new Type())); } template <typename Type> ostream& operator<<(ostream& os, const ObjectPool<Type>& pl) { for (auto elem : pl.pool) os << "{" << elem.first << ", 0x" << elem.second << "} "; return os; } int main() { shared_ptr<ObjectPool<Product>> pool = ObjectPool<Product>::instance(); vector<shared_ptr<Product>> vec(4); for (auto& elem : vec) elem = pool->getObject(); pool->releaseObject(vec[1]); cout << *pool << endl; shared_ptr<Product> ptr = pool->getObject(); vec[1] = pool->getObject(); cout << *pool << endl; }
Минусы После использования объекта мы возвращаем его в пул, и здесь возможна так называемая утечка информации. Мы работали с этим объектом. Вернув его в пул, он находится в том состоянии, с которым мы с ним перед этим работали. Его надо либо вернуть в исходное состояние, либо очистить, чтобы при отдаче этого объекта другому клиенту не произошла утечка информации. Пару комментариев
- Пул объектов - контейнерный класс, удобно использовать итераторы!
- Необходимо знать, занят объект или нет, используем пару: ключ (bool) и объект.