Inicio > Apunte, C++, Independiente > Fábrica de clases

Fábrica de clases


El patrón de diseño de una fábrica de clases, conocido como Método de Fábrica (Factory Method) se emplea cuando tenemos un escenario en el que una clase va a crear una instancia de otra clase, pero en realidad puede ser cualquier clase derivada de ésta, dejando a una implementación en particular que decida qué "versión" (clase derivada) es la que ha de instanciarse. En UML, esto sería algo así.

 

En general, como se muestra en el diagrama (efectívamente fusilado de la Wikipedia), tenemos dos entidades: un "creador" y un "producto". Aquí, el producto es una clase base la cuál será creada por el "creador" dependiendo de ciertos parámetros o circunstancias. Por supuesto, deberá haber clases del producto derivadas, las cuales son las que realmente se van a instanciar. Por su parte, el "creador" define los métodos a través de los cuáles se "configurará" la forma de crear una clase, y eventualmente tendrá un método que sea el que cree la instancia. En algunos casos (como en el del diagrama) se derivarán clases de este creador para determinar la forma de instanciar la clase. Esto llega a pasar, pero a veces no es tan necesario (como lo demostrará el ejemplo de esta entrada). Pero vamos por partes y partamos de una motivación sencilla.

En principio, un método de fábrica de clases es un método que crea una instancia de una clase dados ciertos parámetros. Por ejemplo:

class Producto
{
    private:
        int _productId;
        string _name;
        double _price;

    public:
        Product();
        virtual ~Product();

        int GetProductId() const;
        string GetName() const;
        double GetPrice() const;
        void SetProductId(int id);
        void SetName(const string& name);
        void SetPrice(double price);

        static Product* CreateFromId(int id)
        {
            Product* p = new Product;
            p->SetId(id);
            return p;
        }
};

void foo(int productId)
{
    Product* product = Product::CreateFromId(productId);
    ...
}

Por supuesto, también podríamos haber instanciado a Product con tan solo crear una nueva instancia. En este caso, no tendría mucho sentido emplear un método (aparte del constructor) que nos cree una instancia. Por ello, muchas veces el constructor se declara como privado. Veamos:

class Producto
{
    private:
        int _productId;
        string _name;
        double _price;

        Product();

        void LoadById(int id);
        void LoadByCodebar(const string& codebar);

    public:
        virtual ~Product();

        int GetProductId() const;
        string GetName() const;
        double GetPrice() const;
        void SetProductId(int id);
        void SetName(const string& name);
        void SetPrice(double price);

        static Product* CreateFromId(int id)
        {
            Product* p = new Product;
            p->LoadById(id);
            return p;
        }

        static Product* CreateFromCodeBar(const string& codebar)
        {
            Product* p = new Product;
            p->LoadByCodebar(codebar);
            return p;
        }
};

void foo(int productId)
{
    Product* product = Product::CreateFromId(productId);
    ...
}

void bar(const string& codebar)
{
    Product* product = Product::CreateFromCodebar(codebar)
    ...
}

Ahora sí queda más claro, espero. Vemos que hay dos formas de crear nuestro objeto, por un ID y por un código de barras. Evidentemente la forma en que obtenemos esos datos son irrelevantes, así que no des lata. El caso es que bajo un escenario se crea la clase buscando en la base de datos por el ID y en la otra, por el código de barras. Esta es más o menos la idea, aunque en estos momentos todavía no hemos justificado el hecho de una fábrica de clases. En efecto, esto mismo lo podíamos haber hecho sobrecargando el constructor, ¿no?


Pues bien, ahora miremos el siguiente escenario. Supongamos que tenemos una clase, Customer, y que luce más o menos como sigue:

class Customer
{
    private:
        int _id;
        int _disccountPctge;

        Customer();

    public:
        int GetId() const;
        int GetDisccount() const;
        void SetId(int id);
        void SetDisccount(int disccount);
        bool HasDisccounts() const;
};

Esta clase representa a un usuario del sistema. Vemos que tiene la propiedad _disccountPctge que determina una regla de negocio bajo la cuál al precio de un producto se le hace un descuento determinado por ese valor. El método HasDisccounts determina si un producto tiene descuento o no (analizando _disccountPctge), y en base a eso, obtiene el precio. Una forma polimórfica y orientada a objetos de solucionar esto sería tener algo como:

class Producto
{
    protected:
        double _price;

    public:
        Product();
        virtual ~Product();

        ...
        virtual double GetPrice() const
        {
            return _price;
        }
        ...
};

class ProductDisscount : public Product
{
    private:
        int _disccount;

    public:
        ProductDisccount();
        virtual ~ProductDisccount();

        int GetDisccount() const;
        void SetDisccount(int disccount);
        ...
        virtual double GetPrice() const
        {
            return (Product::GetPrice() * GetDisccount()) / 100;
        }
};

El código anterior nos muestra una clase normalita que tiene su precio. De ahí derivamos una clase que va a aplicar un descuento previamente determinado. Entonces, cuando creemos una instancia de Product, el precio será el que trae el objeto, tal cuál. Pero si creamos una instancia de la clase derivada ProductDisccount, entonces al precio original de la clase se le aplicará el descuento determinado de forma inmediata. Muy interesante, ¿no?

Ahora, regresemos a nuestra clase Customer. En este escenario imaginario, la regla de negocio nos dice que habrá algún cliente que tenga un descuento predefinido, y habrá otros clientes que no tengan dicho descuento. Eso querrá decir que en ciertas circunstancias (cuando el cliente tiene descuento) se empleará una instancia de la clase ProductDisccount, mientras que cuando no tenga descuento, será necesaria una instancia de Product en su lugar.

Pues bien, nos hemos fabricado el escenario perfecto para una fábrica de clases. En efecto, observamos que el empleo de una versión u otra del tipo de producto depende de una variable, a saber, si el cliente tiene o no un descuento. Entonces esto lo podemos hacer transparente creando una clase que decida si crear una u otra instancia, dependiendo de las condiciones del momento. Digamos:

class ProductFactory
{
    private:
        Customer* _customer;

        bool HasDisccount() const
        {
            if (_customer == NULL)
                return false;
            else
                return _customer->HasDisccount();
        }

    public:
        ProductFactory() { _customer = NULL; }
        ProductFactory(Customer* customer) { _customer = customer; }
        virtual ~ProductFactory() { }

        void SetCustomer(Customer* customer) { _customer = customer; }

        Product* CreateProduct()
        {
            Product* product;

            if (HasDisccount())
            {
                ProductDisccount aux = new ProductDisccount();
                aux->SetDisccount(_customer.GetDisccount());
                product = aux;
            }
            else
            {
                product = new Product();
            }

            return product;
        }
};

Y entonces, ahora sí, podemos delegar la responsabilidad del tipo de dato a nuestra fábrica de clases, de forma totalmente transparente.

void foo()
{
    Customer cust1;
    Customer cust2;
    ProductFactory factory;
    Product* product;

    // creamos un cliente que no tenga descuento
    cust1.SetDisccount(0);
    // creamos un cliente que tenga descuento del 25%
    cust2.SetDisccount(25);

    // obtenemos el producto relacionado al primer cliente
    factory.SetCustomer(&cust1);
    product = factory.CreateProduct();
    product->SetPrice(100.0);
    cout << "Precio: " << product->GetPrice() << endl;
    delete product;

    // obtenemos el producto relacionado al segundo cliente
    factory.SetCustomer(&cust2);
    product = factory.CreateProduct();
    product->SetPrice(100.0);
    cout << "Precio: " << product->GetPrice() << endl;
    delete product;

    // obtenemos el producto que no esté relacionado a cliente alguno
    factory.SetCustomer(NULL);
    product = factory.CreateProduct();
    product->SetPrice(100.0);
    cout << "Precio: " << product->GetPrice() << endl;
    delete product;
}

Evidentemente, la salida que daría el programa anterior cuando ejecutase la función foo sería:

Precio: 100.0
Precio: 75.0
Precio: 100.0

Obtuvimos el comportamiento polimórfico que queríamos, pero la función foo ni se preocupó. A ésta solo le interesa que hubiese un producto y que tuviera un precio. Hemos delegado esta responsabilidad a la fábrica de clases.

Las fábricas de clases se emplean en muchos lugares, y es uno de los patrones más empleados. El Component Object Model hace uso de este patrón al crear una clase a partir de un identificador único (GUID, IID). Una aplicación que tenga diversos tipos de documentos podría emplear este patrón para instanciar cada uno de los diferentes tipos dependiendo de lo que seleccione el usuario.

Otro ejemplo sería cuando se tiene un archivo que reúne ciertas características (digamos, es un archivo de imagen) y hay una clase que define el comportamiento estándar (cargar la imagen, guardarla, modificarla, codificar, decodificar, mostrarla en pantalla, etc). Luego se derivan clases de esta para cada tipo particular (digamos, una para los BMPs, otra para JPGs, otra para PNGs, otra para GIFs, etc). Posteriormente habrá una fábrica de clases que creará la instancia adecuada dependiendo del tipo de archivo (por ejemplo, analizando la extensión del mismo).

Por supuesto, el comportamiento se puede amoldar a las necesidades de cada situación. Una situación en la que se emplea una fábrica de clases, aunque no haya clases derivadas, es por ejemplo cuando queremos mantener un registro de todos los objetos que se crean. Digamos, los objetos se crean de forma dinámica, y queremos evitar que haya pérdida de recursos. Bueno, en estos casos una fábrica de clases es útil porque se podría tener un array donde se guarden las referencias, y así cuando se destruya la fábrica, destruir cada uno de los objetos creados. De hecho, si a esto último le agregamos el poder reutilizar objetos, tendríamos otro patrón de diseño, la "alberca de objetos". Pero bueno, esto ya es otra historia. Por el momento ya fue suficiente.

Categorías: Apunte, C++, Independiente Etiquetas: ,
  1. No hay comentarios aún.
  1. No trackbacks yet.

Deja un comentario