Решение для типовой задачи, когда есть интерфейс, реализации интерфейса, и реализации создаются фабрикой по некоторому значению (типу реализации). В решении обеспечивается саморегистрация классов реализаций интерфейса в фабрике, т.о. отпадает необходимость в switch'е в фабрике, и решение становиться максимально открытым для расширения.
Может кому будет полезно. Или кто выскажет конструктивные замечания.
// Некий интерфейс, реализации которого будем создавать с помощью фабрики
struct IHeader
{
typedef boost::shared_ptr<IHeader> Ptr;
virtual ~IHeader() = 0 {};
};
// Интерфейс метакласса реализации IHeader
struct IHeaderWrapper
{
typedef boost::shared_ptr<IHeaderWrapper> Ptr;
virtual ~IHeaderWrapper() = 0 {};
virtual IHeader::Ptr create() = 0;
};
// Фабрика реализаций IHeader
class HeaderFactory
{
private:
typedef std::map<int, IHeaderWrapper::Ptr> map;
static map& wrappers()
{
static map wrappers_instance;
return wrappers_instance;
}
public:
// Метод создания экземпляра
static IHeader::Ptr create(int type)
{
map::iterator iter = wrappers().find(type);
if (wrappers().end() != iter)
return iter->second->create();
else
throw std::runtime_error("unknown header type");
}
// Метод добавления нового типа экземпляра
static void add(int type, IHeaderWrapper::Ptr wrapper)
{
if (!wrappers().insert(std::make_pair(type, wrapper)).second)
throw std::logic_error("Header type added twice");
}
};
// Метакласс реализации IHeader
template<typename HeaderType>
struct HeaderWrapper : IHeaderWrapper
{
virtual IHeader::Ptr create()
{
return IHeader::Ptr(new HeaderType);
}
};
// Базовый класс для реализаций IHeader - обеспечивает саморегистрацию реализаций в фабрике
template<typename Derived>
struct Header : IHeader
{
private:
struct Initializer
{
Initializer()
{
// Саморегистрация
HeaderFactory::add(Derived::TYPE, IHeaderWrapper::Ptr(new HeaderWrapper<Derived>));
}
};
static Initializer initializer;
protected:
~Header()
{
// Надо обязательно заюзать эту переменную
(void)initializer;
}
};
template<typename HeaderType> typename Header<HeaderType>::Initializer Header<HeaderType>::initializer;
// Реализация IHeader
struct Header1 : Header<Header1>
{
static const int TYPE = 1;
// Реализация
};
// Реализация IHeader
struct Header2 : Header<Header2>
{
static const int TYPE = 2;
// Реализация
};
// Реализация IHeader
struct Header3 : Header<Header3>
{
static const int TYPE = 3;
// Реализация
};
int main()
{
IHeader::Ptr h1 = HeaderFactory::create(1);
IHeader::Ptr h2 = HeaderFactory::create(2);
IHeader::Ptr h3 = HeaderFactory::create(3);
IHeader::Ptr h4 = HeaderFactory::create(4); // Тут будет исключение
}
В принципе тут можно уменьшить кол-во классов путём совмещения: например код из HeaderFactory можно поместить в IHeader. Тут так сказать всё разложено по полочкам.
Заполнение фабрики информацией о реализациях происходит во время инициализации глобальных объектов. Т.е. уже к main() она "константная".
Сразу предвижу замечания по поводу оверхеда. Возможно он есть тут. Конечно для тривиальных случаев делать так не стоит. Но для сложных, я считаю такое решение оправдано. Зато это создаёт некую инфраструктуру, с которой "пользовательские классы" становяться проще. Зато тут всё ортогонально и все классы и методы простые, короткие и понятные. А с "более простыми решения" рука об руку идут switch'и, дублирование и другие прелести.
Плюс эту инфрастуктуру можно нагрузить ещё всякими дополнительными бонусами.
1. Например, в фабрику можно поместить метод:
// Фабрика реализаций IHeader
class HeaderFactory
{
public:
// Проверка на существование типа
static bool exists(int type)
{
return wrappers().end() != wrappers().find(type)
}
};
Например, загрузили из БД тип (число), сразу можно проверить, а правилное ли это число, есть ли у нас такой тип.
2. С каждой реализацией можно связать гораздо больше информации, чем просто тип
// Реализация IHeader
struct Header1 : Header<Header1>
{
// Связываем с типом числовое значение
static const int TYPE = 1;
// Связываем с типом другой тип. Например, тип диалога, который может отобразить информацию о данной реализации
typedef Header1Dlg DlgType;
// Связываем с типом некий флаг
static const bool IS_SIMPLE = true;
// Связываем с типом строку, которую мы можем выводить пользователю
static const char* GetName()
{
return "SOME_HEADER";
}
// Реализация
};
Соответственно надо расширить IHeaderWrapper для поддержки этих значений.
3. В фабрику можно добавить метод для получения всех типов:
// Фабрика реализаций IHeader
class HeaderFactory
{
public:
// Получить список всех метаклассов
static std::vector<IHeaderWrapper::Ptr> getAllWrappers();
};
Имея всё это можно написать функцию, например, для заполнения комбобокса информацией о всех существующих типах + с каждым элементом комбобокса связать код этого типа.
Или функцию, которая выберет из всех типов типы, которые отвечают заданному критерию, например, у которых флаг IS_SIMPLE == true;
В общем можно много чего ещё полезного реализовать, причём реализовать так сказать "красиво".
Плюсы:
1. Практически нулевое дублирование, что важно для сопровождаемого кода
2. Инрастуктура функциональная, расширяемая и "умная"
3. Сами классы реализаций интерфейсов абсолютно самодостаточные, самоописываемые, т.е. высокая локальность данных и функций
4. Чтобы добавить новый класс реализации надо просто его описать и его поддержка автоматически появиться везде где она должна быть. Чем не может похвастаться традиционный подход — саму реализацию добавил, добавил её поддержку в фабрику, добавил её поддержку в интерфейс пользователя, а при загрузке из БД забыл.
... что-то уже до целой статьи дотягивает