Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C++

Data Fabrication Framework

5.00/5 (1 vote)
3 Jun 2021CPOL1 min read 4.5K   59  
Automatic mapping some key to the fabric function, and its usage
In this tip, you will learn how to automatically map some key to the fabric function, and how to use it.

Introduction

There are a lot of situations when we should create an instance of the class but the code where this situation arises doesn't know exactly the type of object to create, the only key is known.

For example, we are deserializing the file, which stored some document and encounters a record with the object of a class with the name OrangeCar, thus to read it, we may write something like:

C++
// Let's imagine that we have car hierarchy: 
class Car{};
class OrangeCar : public Car{};
class RedCar : public Car{};

// and we want to read document this object from this hierarchy
Document read(std::istream in)
{
    Document doc;
    std::unique_ptr<Car> car;
    std::string className = ...;
    if (className == "OrangeCar")
    {
        car = std::make_unique<OrangeCar>();
    }
    else if (className == "RedCar")
    {
        car = std::make_unique<RedCar>();
    }
    .....
    else if (className == "CarOf2058year")
    {
        car = std::make_unique<CarOf2058year>();
    }

    car->read(in); // for example
    doc.cars.push_back(car);
}

The new car will be added in 2058 and 2059 and later, thus we should rework this function every time when new car arises. It would be better to have something like:

C++
Document read(std::istream in)
{
    Document doc;
    std::unique_ptr<Car> car;
    std::string className = ...;

    auto car = CarFactory::createObject(className);

    car->read(in); // for example
    doc.cars.push_back(car);
}

Let's build such magic CarFactory!

It would be great to decrease overhead then we want to add a new car class to CarFactory, something like that would be enough:

C++
// NewCar.cpp
class NewCar : public Car {};

// adding NewCar to factory
CarFactory::add<NewCar>("NewCar");

But this function would never be called, to call it, we should initialize some static or global variable, thus rework:

C++
// adding NewCar to factory
auto NewCarrAdded = CarFactory::add<NewCar>("NewCar");

The NewCarrAdded type and size don't play matter at all! The compiler should call CarFactory::add<NewCar>("NewCar");

Thus one of the CarFactory functions are known:

C++
class CarFactory 
{
public:
    struct Added {}; // Added size don't play matter at all
    template<typename NewCar>
    static Added add(std::string key);
    static std::unique_ptr<Car> createObject(std::string key);
};

To make this class more general, one should remove dependencies from class Car and registration key type:

C++
template<typename Key, typename ObjectBase>
class Factory 
{
public:
    using KeyType = Key;
    using ObjectBase = ObjectBaseType;
    using ObjectBasePtr = std::unique_ptr<ObjectBaseType>;

    struct Added {}; // Added size don't play matter at all
    template<typename Object>
    static Added add(KeyType key);
    static ObjectBasePtr createObject(const KeyType& key);
};

Usage of this class is very simple:

  1. Somewhere one should describe concrete factory:
    C++
    using CarFactory = Factory<std::string, Car>;
  2. Add every object inherited from Car to CarFactory:
    C++
    // adding NewCar to factory
    auto NewCarrAdded = CarFactory::add<NewCar>("NewCar");
  3. Create cars:
    C++
    auto car = CarFactory::createObject("NewCar");

Implementation

Who will create objects? Some function like:

C++
template <typename Object>
std::unique_ptr<Object> = createObject() { return std::make_unique<Object>; }

Thus we need a map that matches the key to functions:

C++
template <typename Object>
using Function = std::unique_ptr<Object>(*)();
template <typename Key, typename Function>
using Map = std::unordered_map<Key, Function>;

So the Factory became:

C++
template<typename Key, typename ObjectBase>
class Factory 
{
public:
    using KeyType = Key;
    using ObjectBase = ObjectBaseType;
    using ObjectBasePtr = std::unique_ptr<ObjectBaseType>;
    using CreatorFunction = std::function<ObjectBasePtr()>;

    class Factories
    {
    public:
        using Map = std::unordered_map<KeyType, CreatorFunction>;

        void addCreator(Key key, CreatorFunction function)
        {
            m_factories[std::move(key)] = function;
        }

        CreatorFunction getCreatorFunction(const Key& key) const
        {
            if (auto found = m_factories.find(key); found != m_factories.end())
                return found->second;

            return nullptr;
        }

        const Map& getCreatorFunctions() const
        {
            return m_factories;
        }

    private:
        Map m_factories;
    };

    struct Added {}; // Added size don't play matter at all
    template<typename Object>
    static Added add(KeyType key)
    {
        auto& factories = getFactories();
        // CreatorFunction: creates Object and returns it as pointer to ObjectBase
        static_assert(std::is_base_of<ObjectBase, Object>::value, 
                      "Object must inherit ObjectBase");
        static auto makeObject = [](){ return std::make_unique<Object>(); };
        factories.addCreator(std::move(key), makeObject);
    }
    static ObjectBasePtr createObject(const KeyType& key)
    {
        auto& factories = getFactories();
        if (auto function = factories.getCreatorFunction(key))
        {
            return function();
        }

        return {};
    }

    static typename Base::Factories& getFactories()
    {
        static typename Base::Factories factories;
        return factories;
    }

};

History

  • 2nd June, 2021: Initial version

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)