В этом посте расскажу о проделанном мной пути до получения, имеющего право на жизнь, менеджера объектов с которым удобно работать, и он не выглядит как набор велосипедов и костылей. Осторожно, много кода на C++
EpicFailObjectManager
С чего всё начинается когда молодой, зелёный программист пишет свой первый менеджер для управления объектами? В первую очередь надо применить все свои “знания” ООП и создать базовый объект, от которого в последствии унаследовать остальные. Итак, самый простой вариант:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
include <string> include <iostream> class ObjectBase { public: ObjectBase( const char* type ) { m_type = type; } virtual ~ObjectBase() {}; void PrintMyName() const { std::cout << m_type.c_str()<<std::endl; } private: std::string m_type; }; |
Далее наследуем от него пачку объектов:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
class ObjectOne: public ObjectBase { public: ObjectOne(): ObjectBase( "ObjectOne" ) { } }; class ObjectTwo: public ObjectBase { public: ObjectTwo(): ObjectBase( "ObjectTwo" ) { } }; class ObjectThree: public ObjectBase { public: ObjectThree(): ObjectBase( "ObjectThree" ) { } }; |
И пишем менеджер, который будет создавать эти самые объекты. Как понять какой тип объекта создать? Самая простая мысль это завести перечисление, каждое значение которого будет соответствовать конкретному объекту. Потом тупо switch case и вуаля:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
#include "Objects.h" enum class OBJECT_TYPE { TYPE_OBJECT_ONE, TYPE_OBJECT_TWO, TYPE_OBJECT_THREE }; class MyEpicFailManager { public: ObjectBase* CreateObject( OBJECT_TYPE type ) { switch( type ) { case OBJECT_TYPE::TYPE_OBJECT_ONE: return new ObjectOne(); case OBJECT_TYPE::TYPE_OBJECT_TWO: return new ObjectTwo(); case OBJECT_TYPE::TYPE_OBJECT_THREE: return new ObjectThree(); } return nullptr; } }; |
Попробуем использовать новоиспеченный менеджер:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
#include "MyEpicFailManager.h" int main() { MyEpicFailManager manager; ObjectBase* ptr_base_one = manager.CreateObject( OBJECT_TYPE::TYPE_OBJECT_ONE ); ObjectBase* ptr_base_two = manager.CreateObject( OBJECT_TYPE::TYPE_OBJECT_TWO ); ObjectBase* ptr_base_three = manager.CreateObject( OBJECT_TYPE::TYPE_OBJECT_THREE ); ptr_base_one->PrintMyName(); ptr_base_two->PrintMyName(); ptr_base_three->PrintMyName(); std::cin.ignore(); return 0; } |
Подведём итог нашего, мягко сказать упоротого менеджера. Самая большая проблема, для того что бы добавить новый тип объекта надо: добавить его в перечисление, изменить функцию создания, что ведёт за собой перекомпиляцию менеджера, т.е. никакой модульности и расширяемости. В общем полный провал.
FailObjectManager
Следующая мысль которая меня посетила, надо избавиться от перечисления, перевести всё на строку и будет счастье. И вынести функцию создания объектов из самого менеджера. Тогда компилируем менеджер один раз, а функцию создания храним в конкретном проекте.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
#include "Objects.h" class MyFailManager { public: // Тип для функции создания typedef ObjectBase* ( *TCreateFunction )( const char* type ); public: // Устанавливаем функцию создания. void SetCreateFunction( TCreateFunction function ) { m_func_creator = function; } ObjectBase* CreateObject( const char* type ) { return m_func_creator( type ); } private: TCreateFunction m_func_creator; }; |
Ну вот, чисто в теории, мы получили менеджер, который может создавать любые объекты унаследованные от ObjectBase. Примерно на таком менеджере у нас вышло пара игр 🙂 Но проблем всё равно вагон и маленькая тележка. Во первых тот самый статический TYPE, который нужно по любому создавать для каждого объекта. Во вторых функция создания, на каждый новый проект её приходится писать заново. т.е. даже если какие-либо объекты уже устаканились и являются универсальными и не относятся, например к какой либо игре, их нужно постоянно учитывать в этой функции. Теперь самая главная задача избавиться от этой функции. Что бы мы могли просто создать новый header и cpp файл с реализацией объекта и всё. Не надо было куда то перекидывать указатели и пр.
ObjectManager
И в этом нам помогут шаблоны, тот самый тёмный лес языка cpp. Мы сделаем один шаблонный класс менеджера, который сможет создавать объекты абсолютно любого типа. Поехали.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
#include <string> #include <map> /*! Универсальный менеджер для создания объектов.*/ template<class BaseT> class TManager { public: /*! Создать объект типа type */ static BaseT* Create( const std::string& type ); private: /// Определяем тип для функции создания объектов typedef BaseT* TFunction(); /// Словарь "Название объекта" - Функция для создания typedef std::map<std::string, TFunction*> TMapFunction; static TMapFunction& GetMap() { static TMapFunction creators; return creators; } }; template<class BaseT> BaseT* TManager<BaseT>::Create( const std::string& type ) { auto iter = GetMap().find( type ); if( iter == GetMap().end() ) return nullptr; return iter->second(); } |
Для начала создаём простой шаблонный класс. Основная идея тут в том, что у этого шаблонного класса есть словарь ключ-значение ( см. функцию GetMap() ) в котором ключём является тип объекта в виде строки, а значением – указатель на функцию, которая создаст объект данного типа. В функции Create() мы ищем по имени эту функцию и вызываем её. Всё достаточно просто. Но вот вопрос, как генерировать функцию, которая собственно будет создавать объекты нужного типа? Продолжаем расширять наш класс.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 |
/*! Универсальный менеджер для создания объектов.*/ template<class BaseT> class TManager { public: /*! Создать объект типа type */ static BaseT* Create( const std::string& type ); private: /// Определяем тип для функции создания что бы положить её в список typedef BaseT* TFunction(); /// Словарь "Название объекта" - Функция для создания typedef std::map<std::string, TFunction*> TMapFunction; private: static TMapFunction& GetMap() { static TMapFunction creators; return creators; } template<class T> class Register { public: static TFunction* CREATOR; static BaseT* Create() { return new T(); }; static TFunction* InitCreator( const std::string& type ) { return GetMap()[type] = Create; } }; }; template<class BaseT> BaseT* TManager<BaseT>::Create( const std::string& type ) { auto iter = GetMap().find( type ); if( iter == GetMap().end() ) return nullptr; return iter->second(); } |
Добавляем еще один внутренний шаблонный класс Register. Исходя из названия можно понять что служит класс именно для того, что бы заводить новые функции создания (Create). Функция InitCreator это то, что нам нужно вызвать для добавления нового типа в менеджер объектов. Сама регистрация нового типа это достаточно большая и не читаемая строчка на шаблонах и что бы упростить её делаем макрос принимающий три значения 1. Экземпляр менеджера. 2. Тип регистрируемого объекта. 3. Строка-ключ, обычно строковое представление типа.
1 2 |
/*! Макрос для упрощения регистрации новых типов */ #define TMANAGER_REGISTER_NEW_TYPE( MANAGER, EXTENDED_TYPE, STRING ) template<> template<> MANAGER::TFunction* MANAGER::Register<EXTENDED_TYPE>::CREATOR = MANAGER::Register<EXTENDED_TYPE>::InitCreator( STRING ) |
Пример использования.
Объявляем базовый класс для объектов. И сразу объявляем тип для менеджера, который способен создавать объекты унаследованные от данного класса.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
//ObjectBase.h #include "TManager.h" class ObjectBase { public: ObjectBase() {} virtual ~ObjectBase() {} virtual void PrintMyType() = 0; }; // Создаём менеджер создания объектов унаследованных от ObjectBase typedef TManager<ObjectBase> ObjectBaseManagerT; |
И делаем для примера два класса унаследованных от ObjectBase. Для каждого класса после его объявления вызываем макрос регистрации в менеджере объектов.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 |
// ObjectOne.h #include "ObjectBase.h" class ObjectOne: public ObjectBase { public: ObjectOne(); virtual ~ObjectOne(); virtual void PrintMyType(); }; // Регистрируем новый тип в менеджере объектов TMANAGER_REGISTER_NEW_TYPE( ObjectBaseManagerT, ObjectOne, "ObjectOne" ); //------------------------------------------------------------------ //ObjectOne.cpp #include "ObjectOne.h" #include <iostream> ObjectOne::ObjectOne(){} ObjectOne::~ObjectOne(){} void ObjectOne::PrintMyType() { std::cout << "print from ObjectOne"<<std::endl; } //------------------------------------------------------------------ // ObjectTwo.h #include "ObjectBase.h" class ObjectTwo: public ObjectBase { public: ObjectTwo(); virtual ~ObjectTwo(); virtual void PrintMyType(); }; // Регистрируем новый тип в менеджере объектов TMANAGER_REGISTER_NEW_TYPE( ObjectBaseManagerT, ObjectTwo, "ObjectTwo" ); //------------------------------------------------------------------ //ObjectTwo.cpp #include "ObjectTwo.h" #include <iostream> ObjectTwo::ObjectTwo(){} ObjectTwo::~ObjectTwo(){} void ObjectTwo::PrintMyType() { std::cout << "print from ObjectTwo"<<std::endl; } |
Ну и теперь пробуем использовать
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
#include <iostream> #include "ObjectBase.h" int main() { ObjectBaseManagerT manager; ObjectBase* object_one = manager.Create( std::string( "ObjectOne" ) ); ObjectBase* object_two = manager.Create( std::string( "ObjectTwo" ) ); object_one->PrintMyType(); object_two->PrintMyType(); delete object_one; delete object_two; std::cin.ignore(); return 0; } |
На экране должно появиться две надписи:
1 2 |
print from ObjectOne print from ObjectTwo |
Вот собственно и всё. В итоге получили легко расширяемую фабрику по созданию объектов. Может она еще не идеальна, но это пока лучшее найденное решение.