template<class Cpp, class Qt, class ... Y_Mas>

Patrón factory basado en templates

Patrón factory basado en templates

    Explicamos una implementación del patrón factory basada en templates.

El patrón factory nos permite la creación de objetos de un subtipo concreto. Entre las diversas ventajas de este patrón, resaltaré dos:

  • Evita exponer la implementación de los subtipos.
  • Permite diferir la elección del subtipo al tiempo de ejecución, por ejemplo, basándose en un identificador de tipo.

Implementación

Para ilustrar este patrón de diseño expondré cómo podemos crear una factoría de formas geométricas. A saber, el cliente tiene expuestos los ficheros shape.h (donde se declara una interfaz IShape que cumplirán todos los objetos) y factory.h (donde se define la función factoría que creará dichos objetos). Los subtipos específicos no están expuestos a los clientes; para construirlos hay que invocar a la función factoría make_shape con el identificador del tipo deseado: triangle, square, circle.

std::unique_ptr<IShape> make_shape(std::string const& id);

// ...
auto triangle = make_shape("triangle");

Propuesta inicial

Una primera aproximación sería definir el método make_shape a pelo, listando todas los subtipos soportados:

#include "factory.h"
#include "Triangle.h"
#include "Square.h"
#include "Circle.h"

std::unique_ptr<IShape> make_shape(std::string const& id)
{
    if (id == "triangle") { return std::make_unique<Triangle>(); }
    if (id == "square") { return std::make_unique<Square>(); }
    if (id == "circle") { return std::make_unique<Circle>(); }
    return nullptr;
}

Primera mejora: extracción del ID

Esta implementación básica es suficiente para cubrir las necesidades más inmediatas. Vamos a buscarle el primer pero: el identificador del tipo no debería estar en la factoría, sino con el tipo. De este modo si el identificador se necesita en otro lugar (construcción de un JSON, datos de depuración o log, etc.), no habría que duplicarlo.

Supongamos que se establece que todas las sub-clases de IShape deben definir un método estático id() que nos devuelva el ID del objeto. De esta forma podríamos tener un código más independiente:

std::unique_ptr<IShape> make_shape(std::string const& id)
{
    if (id == Triangle::id()) { return std::make_unique<Triangle>(); }
    if (id == Square::id()) { return std::make_unique<Square>(); }
    if (id == Circle::id()) { return std::make_unique<Circle>(); }
    return nullptr;
}

Esto mejora mucho la parte del ID, pero nos deja ahora el segundo pero: la duplicación de código.

Segunda mejora: automatizar la factoría

La duplicación de código antes mencionada puede ser evitada mediante un proceso que se llama registro de clases. En éste se asocian los ID de las clases con un método creador, de forma que dicho método puede ser usado a posteriori. Para ello crearemos una clase privada ShapeFactory que llevará un registro de todas las clases soportadas:

class ShapeFactory
{
public:
    ShapeFactory()
    {
        register_class<Triangle>();
        register_class<Square>();
        register_class<Circle>();
    }

    std::unique_ptr<IShape> make(std::string const& id) const
    {
        auto it = m_creators.find(id);
        if (it == m_creators.end())
        {
            // ID not found
            return nullptr;
        }

        return it->second();
    }

private:
    template<class T>
    void register_class()
    {
        m_creators.emplace(T::id(), []() { return std::make_unique<T>(); });
    }

    std::map<std::string, std::function<std::unique_ptr<IShape()>>> m_creators;
};

std::unique_ptr<IShape> make_shape(std::string const& id)
{
    static ShapeFactory factory{};
    return factory.make(id);
}

Se puede probar un ejemplo en Coliru.

Tercera mejora: factoría genérica

Imaginemos que en nuestro código tenemos un par de docenas de interfaces a las que queremos asociar un método factory. La factoría antes presentada funciona muy bien para crear objetos cuyo subtipo implementa la interfaz IShape, pero no para otros tipos, por lo que tendríamos que duplicar dicho código para cada nueva interfaz y los subtipos asociados.

Si examinamos detenidamente la implementación de la ShapeFactory, podremos ver rápidamente que es fácilmente generalizable si convertimos la clase factoría en una clase templatizada. Usando variadic templates para especializar la factoría con múltiples tipos asociados tenemos algo como:

template<class... Ts>
class Factory
{
    // ...
};

std::unique_ptr<IShape> make_shape(std::string const& id)
{
    using factory_t = Factory<Triangle, Square, Circle>;
    static factory_t factory{};
    return factory.make(id);
}

Ahora, el problema radica en registrar las clases de la factoría. Para ello, necesitamos poder iterar sobre todos los tipos indicados. Esto se consigue mediante una función tonta (vacía) que nos haga las veces de expansor, llamándola con una función que se ejecute una vez para cada tipo de dato en el template. El truco acá consiste en que dicha función debe devolver un valor (cualquier cosa menos void), para poder expandirse como lista de argumentos.

template<class... Ts>
class Factory
{
public:
    Factory()
    {
        register_all(register_class<Ts>()...);
    }

    // ...

private:
    template<class... Ts>
    void register_all(Ts...)
    {
    }

    template<class T>
    auto register_class()
    {
        return m_creators.emplace(T::id(), []() { return std::make_unique<T>(); }).second;
    }

    // ...
};

En este caso el método register_class devuelve un booleano indicado si la clase aún no había sido registrada, aunque realmente se usa únicamente para el truco de la expansión de parámetros, no para una verificación real.

Dicho lo anterior, ahora se pueden crear tantos métodos factory como se deseen con una mínima inversión: únicamente hay que especializar la clase Factory en el método factoría de cada interfaz.

Como nota final, podéis consultar un ejemplo completo operativo de patrón factory en Coliru.