Archive

Archive for the ‘Independiente’ Category

Filtrando contenedores mediante boost::filter_iterator


Cuando creamos nuestro modelo de datos, a veces nos encontramos con que necesitamos crear clases que actúan como contenedores. En estos casos lo natural que expongamos una forma a través de la cual podamos acceder a los objetos contenidos. Pero la mayoría de las veces (a menos que se trate de un contenedor trivial) también querremos exponer una forma para filtrar el contenido por criterios definidos.

En el caso de que el contenedor sea trivial y sólo necesites acceder al contenido, nos basta entonces con usar alguno de los contenedores estándares que la librería estándar de C++ pone a nuestra disposición, como std::list o std::vector. Pero ¿y cuando queremos añadir filtros? Por supuesto que hay varias formas de hacerlo, pero eso lo veremos más adelante. Comencemos por armar nuestro ejemplo base.

Pensemos en una clase empleado que tiene algunos atributos: nombre, edad, salario y un puntero hacia el jefe (o nulo si no lo tiene); y un método que nos permita imprimir el nombre del empleado hacia la consola. Nothing too fancy:


class employee
{
    public:
        employee()
            : income(0.0), age(0), manager(nullptr)
        { }
        employee(const string& name, double income, int age, employee* manager)
            : name(name), income(income), age(age), manager(manager)
        { }
        employee(const employee& copy)
            : name(copy.name), income(copy.income), age(copy.age), 
              manager(copy.manager)
        { }

        string manager_name() const { 
            return manager != nullptr ? manager->name : "[Sin jefe]"; 
        }

        void print() const { cout << name << endl; }

        string name;
        double income;
        int age;
        employee* manager;
};

Nada del otro mundo. Ahora pensemos en cómo sería nuestra clase contenedora. La librería estándar de C++ nos provee los contenedores de la STL, como std::list o std::vector. Todos estos contenedores se basan en la idea de separar el acceso a los datos del algoritmo empleado para almacenarlos. En otras palabras, la forma en la que obtenemos los objetos no varía respecto de la forma en la que éstos se almacenan (en nodos, en bloques de memoria adyacentes, etc.).

La forma de acceder a los objetos se hace mediante iteradores. El concepto de un iterador es el de un puntero hacia el siguiente elemento de un contenedor (iterador de avance), hacia el siguiente y el anterior (iterador bidireccional), o hacia un elemento particular (iterador de acceso aleatorio). De esta forma, dado un elemento cualquiera dentro de la colección siempre será posible acceder al que le sigue, sin importar la forma en la que esté implementado el algoritmo interno.

Dado lo anterior, es una buena idea seguir el mismo patrón para nuestra clase contenedora. Aunque internamente usaremos un std::list para almacenar el contenido.



class catalog
{
    public:
        typedef list<employee> employee_list;
        typedef employee_list::size_type size_type;
        typedef employee_list::iterator iterator;
        typedef employee_list::const_iterator const_iterator;

    private:
        employee_list _employees;
        
    public:

        catalog() { }

        size_type size() const { return _employees.size(); }
        void clear() { _employees.clear(); }

        void add(const employee& emp) { _employees.push_back(emp); }
        void add(const string& name, double income, int age, employee* mgr) {
            add(employee(name, income, age, mgr));
        }
};

Bueno, sólo son algunos typedefs y algunos métodos comúnmente encontrados en este tipo de clases. Ahora bien, queremos exponer los elementos del catálogo, así que bien haríamos con añadir un método begin_all y end_all que nos devuelva el iterador inicial y el final.


        iterator begin_all() { return _employees.begin(); }
        iterator end() { return _employees.end(); }

Bueno, ahora sí. ¿Cómo le hacemos para añadir filtros? Por ejemplo, ¿cómo le hacemos si quisiéramos, de alguna forma, exponer a aquellos empleados cuyo nombre contiene alguna letra o cuya edad se encuentra en un rango determinado?

Una forma, por supuesto, sería crear un método similar a este:

...

void fill_by_age(list& lst, int min, int max)
{
    for (auto i = employees.begin(); i != employees.end(); ++i)
    {
        if (min <= age && i->age <= max)
            lst.push_back(*i);
    }
}
...

Aunque correcto, este código presenta ciertas deficiencias de diseño. Para empezar tenemos que pasar una referencia al contenedor donde se guardarán. Para seguir, tenemos que especificar el tipo de contenedor que usaremos (std::list). Si mañana quisiéramos cambiar de un list a un vector o a un map, rompería código ya escrito y habría que rehacerlo. Y por último, aquí estamos rompiendo la regla de separar implementación del acceso.

No hay mucho qué hacer para el primer problema. Pero para resolver el segundo, podríamos crear un método plantilla y que ahí se decida el tipo de contenedor. Y aun si obviáramos los problemas de estar manejando plantillas, no resolveríamos el tercer problema.

¿Qué podemos hacer? Bueno, una alternativa es regresar iteradores, similares al begin_all y end_all que hicimos hace rato. Pero en aquel momento simplemente pasamos los iteradores del contenedor interno (std::list). ¿Qué hacer ahora? ¿Crear nuestros propios iteradores, unos que hagan el filtro en automático?

Evidentemente es una opción, aunque es cierto que es una un tanto compleja y laboriosa. Y aquí el estándar de C++ nos deja sin opciones. Afortunadamente aquí es donde entra Boost. Esta biblioteca cuenta con una clase que nos ayuda, precísamente, a hacer esto que queremos. Estamos hablando de boost::filter_iterator<P, I>.

No voy a entrar en una discusión completa sobre esta clase (para eso, mejor revisar la documentación de Boost). Pero sí diré que es una clase que toma un predicado o functor (de tipo P), y un iterador de tipo I. La clase recorre el iterador, de inicio a fin, y para cada elemento ejecuta el predicado / functor en cuestión. Si éste regresa verdadero, filter_iterador añade el elemento a un iterador temporal. Si no, lo deja fuera. Luego, puede recorrerse este iterador temporal y todo lo que obtendremos serán los elementos filtrados.

¡Yay! ¡Suena como a algo que necesitamos! ¿Cómo lo implementamos?

Lo primero que necesitamos es un functor. Es decir, una clase con el operador () sobrecargado, y que tome como parámetro un objeto del contenedor, es decir, de tipo employee. Esta sobrecarga deberá regresar true para cuando queramos que el objeto pasado como parámetro sea incluido en el filtro. Así que podemos probar con un functor para filtrar la edad por un rango mínimo (ésto último pasados como parámetros al constructor del functor).

struct range_func
{
    range_func()
        : _min(0), _max(0)
    { }
    range_func(const range_func& copy)
        : _min(copy._min), _max(copy._max) 
    { }
    range_func(int min, int max)
        : _min(min), _max(max) 
    { }

    bool operator()(const employee& emp) const {
        return _min <= emp.age && emp.age <= _max;
    }

    int _min;
    int _max;
};

Sencillo, ¿no? Bueno, ahora lo que haremos es crear nuestro boost::filter_iterator para devolver un iterador de inicio y uno de fin. Para ello, usaremos la función boost::make_filter_iterator, en donde le pasaremos el functor ya creado y luego el iterador. Para el begin(), le pasamos begin() y end() de la lista, y para el end(), pasamos dos veces el end() de la lista. Algo así.

typedef filter_iterator range_filter_iterator;
...

range_filter_iterator begin_age_range(int min, int max)  {
	return begin_age_range(range_func(min, max));
}
		
range_filter_iterator begin_age_range(const range_func& func) {
	return make_filter_iterator<range_func>((func, 
        _employees.begin(), _employees.end());
}

range_filter_iterator end_age_range(int min, int max) {
    return end_age_range(range_func(min, max));
}

range_filter_iterator end_age_range(const range_func& func) {
    return make_filter_iterator<range_func>((func, 
        _employees.end(), _employees.end());
}

Un typedef al inicio para facilitar las cosas. Por lo demás, make_filter_iterator se encarga del resto. Ahora podemos usar nuestra clase de una forma similar a esta.

int main(int argc, char* argv[])
{
    catalog cat;
    employee fer("Fernando", 10000, 29, nullptr);
    cat.add(fer);
    cat.add("Gabriela", 13500, 26, &fer);
    employee jime("Jimena", 9500, 22, &fer);
    cat.add(jime);
    cat.add("Mario", 4300, 37, &jime);
    employee caty("Catalina", 15000, 31, nullptr);
    cat.add(caty);
    employee gis("Gisela", 14750, 30, &caty);
    cat.add(gis);
    cat.add("Omar", 12700, 30, &gis);

    auto func = [](const employee& emp) { 
        emp.print(); 
    };

    cout << "Edad entre 25 y 30" << endl;
    for_each(cat.begin_age_range(25, 30), cat.end_age_range(25, 30), func);
    cout << endl;

    ...
    return 0;
}

Como podemos ver, filter_iterator es una forma fácil de generar filtros y de paso resolver los tres problemas planteados hace un momento: no se expone el tipo del contenedor, y mantenemos separada la implementación.

Ya para terminar, dejo un programita de ejemplo completo: filtros por edad, nombre y jefe.



#include <iostream>
#include <string>
#include <list>
#include <functional>
#include <algorithm>
#include <boost/iterator/filter_iterator.hpp>

using std::string;
using std::cout;
using std::endl;
using std::list;
using std::function;
using std::for_each;
using std::shared_ptr;
using boost::filter_iterator;
using boost::make_filter_iterator;

class employee;

class employee
{
    public:
        employee()
            : income(0.0), age(0), manager(nullptr)
        { }
        employee(const string& name, double income, int age, employee* manager)
            : name(name), income(income), age(age), manager(manager)
        { }
        employee(const employee& copy)
            : name(copy.name), income(copy.income), age(copy.age), 
                manager(copy.manager)
        { }

        string manager_name() const { 
            return manager != nullptr ? manager->name : "[Sin jefe]"; 
        }

        void print() const { cout << name << endl; }

        string name;
        double income;
        int age;
        employee* manager;
};

struct range_func
{
    range_func()
        : _min(0), _max(0)
    { }
    range_func(const range_func& copy)
        : _min(copy._min), _max(copy._max) 
    { }
    range_func(int min, int max)
        : _min(min), _max(max) 
    { }

    bool operator()(const employee& emp) const {
        return _min <= emp.age && emp.age <= _max;
    }

    int _min;
    int _max;
};

struct substr_func
{
    substr_func() 
    { }
    substr_func(const string& str) 
        : _str(str)
    { }
    substr_func(const substr_func& copy)
        : _str(copy._str)
    { }

    bool operator()(const employee& emp) const {
        return emp.name.find(_str) != string::npos;
    }

    string _str;
};

struct mgr_func
{
    mgr_func()
        : _ptr(nullptr)
    { }
    mgr_func(employee* ptr)
        : _ptr(ptr)
    { }
    mgr_func(const mgr_func& copy)
        : _ptr(copy._ptr)
    { }

    bool operator()(const employee& emp) const {
        bool val = false;
        if (_ptr == nullptr)
            val = emp.manager == nullptr;
        else 
            val = emp.manager_name() == _ptr->name;
        return val;
    }

    employee* _ptr;
};


class catalog
{
    public:
        typedef list<employee> employee_list;
        typedef employee_list::size_type size_type;
        typedef employee_list::iterator iterator;
        typedef employee_list::const_iterator const_iterator;
        typedef filter_iterator<range_func, iterator> range_filter_iterator;
        typedef filter_iterator<substr_func, iterator> substr_filter_iterator;
        typedef filter_iterator<mgr_func, iterator> mgr_filter_iterator;

    private:
        employee_list _employees;
        
    public:

        catalog() { }

        size_type size() const { return _employees.size(); }
        void clear() { _employees.clear(); }

        void add(const employee& emp) { _employees.push_back(emp); }
        void add(const string& name, double income, int age, employee* mgr) {
            add(employee(name, income, age, mgr)); 
        }

        iterator begin_all() { return _employees.begin(); }
        iterator end() { return _employees.end(); }

        range_filter_iterator begin_age_range(int min, int max)  {
			return begin_age_range(range_func(min, max));
		}
		
		range_filter_iterator begin_age_range(const range_func& func) {
			return make_filter_iterator<range_func>(func, _employees.begin(), 
                _employees.end());
		}

        range_filter_iterator end_age_range(int min, int max) {
            return end_age_range(range_func(min, max));
        }

        range_filter_iterator end_age_range(const range_func& func) {
            return make_filter_iterator<range_func>(func, _employees.end(), 
                _employees.end());
        }

        substr_filter_iterator begin_substr(const string& value) {
            return begin_substr(substr_func(value));
        }

        substr_filter_iterator begin_substr(const substr_func& func) {
            return make_filter_iterator(func, _employees.begin(), 
                _employees.end());
        }

        substr_filter_iterator end_substr(const string& value) {
            return end_substr(substr_func(value));
        }

        substr_filter_iterator end_substr(const substr_func& func) {
            return make_filter_iterator(func, _employees.end(),
                 _employees.end());
        }

        mgr_filter_iterator begin_mgr(employee* mgr) {
            return begin_mgr(mgr_func(mgr));
        }

        mgr_filter_iterator begin_mgr(const mgr_func& func) {
            return make_filter_iterator(func, _employees.begin(), 
                _employees.end());
        }

        mgr_filter_iterator end_mgr(employee* mgr) {
            return end_mgr(mgr_func(mgr));
        }

        mgr_filter_iterator end_mgr(const mgr_func& func) {
            return make_filter_iterator(func, _employees.end(), 
                _employees.end());
        }
};

int main(int argc, char* argv[])
{
    catalog cat;
    employee fer("Fernando", 10000, 29, nullptr);
    cat.add(fer);
    cat.add("Gabriela", 13500, 26, &fer);
    employee jime("Jimena", 9500, 22, &fer);
    cat.add(jime);
    cat.add("Mario", 4300, 37, &jime);
    employee caty("Catalina", 15000, 31, nullptr);
    cat.add(caty);
    employee gis("Gisela", 14750, 30, &caty);
    cat.add(gis);
    cat.add("Omar", 12700, 30, &gis);

    auto func = [](const employee& emp) { 
        emp.print(); 
    };

    cout << "*** Todos los empleados *** " << endl;
    for_each(cat.begin_all(), cat.end(), func);
    cout << endl;

    cout << "***** POR EDAD *****" << endl;

    cout << "Edad entre 25 y 30" << endl;
    for_each(cat.begin_age_range(25, 30), cat.end_age_range(25, 30), func);
    cout << endl;

    cout << "Edad menor a 25" << endl;
    for_each(cat.begin_age_range(0, 25), cat.end_age_range(0, 25), func);
    cout << endl;

    cout << "Edad mayor a 30" << endl;
    for_each(cat.begin_age_range(30, 100), cat.end_age_range(30, 100), func);
    cout << endl;

    cout << "***** POR NOMBRE *****" << endl;

    cout << "Nombre que contenga 'na'" << endl;
    for_each(cat.begin_substr("na"), cat.end_substr("na"), func);
    cout << endl;

    cout << "Nombre que contenga 'ri'" << endl;
    for_each(cat.begin_substr("ri"), cat.end_substr("ri"), func);
    cout << endl;

    cout << "***** POR JEFE *****" << endl;

    cout << "Le reportan a Fernando" << endl;
    for_each(cat.begin_mgr(&fer), cat.end_mgr(&fer), func);
    cout << endl;

    cout << "Le reportan a Caty" << endl;
    for_each(cat.begin_mgr(&caty), cat.end_mgr(&caty), func);
    cout << endl;

    system("pause");
    return 0;
}


Y al ejecutarlo, tenemos este resultado:

*** Todos los empleados ***
Fernando
Gabriela
Jimena
Mario
Catalina
Gisela
Omar

***** POR EDAD *****
Edad entre 25 y 30
Fernando
Gabriela
Gisela
Omar

Edad menor a 25
Jimena

Edad mayor a 30
Mario
Catalina
Gisela
Omar

***** POR NOMBRE *****
Nombre que contenga 'na'
Fernando
Jimena
Catalina

Nombre que contenga 'ri'
Gabriela
Mario

***** POR JEFE *****
Le reportan a Fernando
Gabriela
Jimena

Le reportan a Caty
Gisela

Press any key to continue . . .

Ahora, intenta hacer algo similar sin functores, sino con lambdas… o mediante std::function. ¡Disfruta!

Categorías:Apunte, Boost, C++, Independiente Etiquetas: , ,

Functores y objetos llamables en la librería estándar de C++


Hace algunos días escribí acerca de los lambdas de C++ y C#. En esa oportunidad hice un recuento sobre las opciones que en C++ existen para tratar funciones como parámetros, o en general, poder tener “objetos llamables” como variables. En particular, exploramos tres alternativas:

1.- Funciones de retro-llamada. También llamados “callbacks”, estas funciones son los famosos punteros a void* (o al tipo de dato que retornan).

void print(const string& str)
{
    cout << str << endl;
}

void foo()
{
    void (*func)(const string& str);
    func = &print;
    func("Hola mundo!");
}

2.- Functores. Estas son clases que implementan el operador (), de tal suerte que simulan ser funciones, aunque en realidad sean clases.

class printer
{
    public:
        string str;

        printer() { };
        printer(const string& msg) : str(msg) { }

        void operator()() { 
            cout <str <str = msg;
            cout <str << endl;
        }
};

int main()
{
    printer func;
    func("Hola mundo!");

    func.str = "Hallo Welt!";
    func();

    return 0;
}

3.- Lambdas. Como comentamos, éstas son funciones anónimas cuyo cuerpo se define al momento, i.e. dentro de otra función.

auto func = [](const string& str) {
    cout << str << endl;
};

func("Hola mundo!");

Pues bien, hay una opción de la que no he hablado. En esencia no se diferencia mucho de un functor. O, mejor dicho, es un functor. O aún mejor dicho, es una plantilla que define un objeto que puede ser llamado, como una función o functor. ¡Argh! Me refiero a la clase function, definida en <functional>.

Esta clase es una plantilla cuyo parámetro es –escúchalo bien- la definición de una funcion. Es decir, su firma. Y obvio con esa firma es con la que se define el operador (). Por cierto, el operador de asignación está sobrecargado para recibir la función a ejecutar. Así es, a function de todas formas le tienes que pasar una función como parámetro. Veamos.

#include <iostream>
#include <functional>

using std::cout;
using std::endl;
using std::function;

int sqr(int a)
{
    return a * a;
}

int main(int argc, char* argv[])
{
    function f;
    f = sqr;
    cout << f(5) << endl;

    system("pause");
    return 0;
}

Obvio se imprime “25” en pantalla. En la primera línea definimos nuestra función con parámetro de plantilla int(int). Esto quiere decir que la función tendrá un parámetro de tipo entero y que regresa otro entero. La segunda línea asigna la función sqr (que cumple con la firma de la plantilla). La tercera línea simplemente invoca la función.

Si creas una función, no le asignas la referencia e intentas invocarla, te lanzará una excepción de tipo bad_function_call.

try
{
    function<int(int)> f;
    int a = f(5); // tsssss!
    cout << a << endl;
}
catch (const std::bad_function_call& ex)
{
    cout << ex.what() << endl;
}

Lo mejor: ¡también podemos usar lambdas!

typedef function<int(int, int)> bin_function;

bin_function suma;
suma = [](int a, int b) { return a + b; };

bin_function resta;
resta = [](int a, int b) { return a - b; };

cout << suma(31, 11) << endl;
cout << resta(67, 25) << endl;

Ahora bien, ¿qué ventaja tiene function<T> sobre usar lambdas? Pues que por un lado hace innecesario tener que crear nuestros propios functores de nuevo, y por otro lado, podemos usarlo como parámetro. Por ejemplo, para crear eventos o una implementación del patrón “observer”.

Veamos. Primero nos creamos una clase sencilla: empleado, la cual tiene tres miembros: para nombre, edad y domicilio. Privados. Luego creamos un par de métodos para cada una de éstas: uno que obtiene el dato y otro que lo escribe. Lo que queremos es que cada vez que alguna de estas tres propiedades se actualice, si el dato es diferente, entonces así lo notifique a quien quiera “escucharlo”. Al final, es es lo que hace el patrón de diseño observer. En fin, para ello creamos un método al cual se le llamará cuando cambien las propiedades. Algo así.

class employee
{
    public:
        employee() { }

        string name() const { return _name; }
        string address() const { return _address; }
        int age() const { return _age; }

        void name(const string& value) {
            if (value != _name) {
                _name = value;
                on_property_changed("name");
            }
        }

        void address(const string& value) {
            if (value != _address) {
                _address = value;
                on_property_changed("address");
            }
        }

        void age(int value) {
            if (value != _age) {
                _age = value;
                on_property_changed("age");
            }
        }
    protected:
        void on_property_changed(const string& property_name)
        {
        }

    private:
        string _name;
        string _address;
        int _age;
};

Pero ¿qué ponemos en on_property_changed? Lo que queremos es avisar que el objeto ha sido actualizado, así que esta función debería llamar a los “escuchas” o “listeners” pasándole una referencia a sí mismo. Luego, queremos pasar el nombre de la propiedad que ha cambiado. Para todo esto, quizás sea mejor crear una clase: la llamaremos event_args.

class event_args
{
    public:
        event_args(const employee& sender, const string& property_name)
            : _sender(sender), _property_name(property_name)
        {
        }

        const employee& sender() const { return _sender; }
        string property_name() const { return _property_name; }

    private:
        const employee& _sender;
        string _property_name;
};

Y ahora, nos basta un par de typedefs:  uno que defina una función (std::function<T>, recuerda) que tome como parámetro un objeto de tipo event_args, y luego, un contenedor de este tipo de funciones, como –digamos- std::list.

typedef function<void(event_args&)> event_function;
typedef list<event_function> event_listeners;

Ahora en la clase employee nos creamos un miembro (público, por facilidad) de tipo event_listeners…

event_listeners listeners;

…y procedemos a escribir, ahora sí, nuestra función on_property_changed, que recorra cada función de la lista y la invoque. Easy peasy.

void on_property_changed(const string& property_name)
{
    event_args args(*this, property_name);
            
    auto callback = [&args](event_function& func) { func(args); };
    for_each(listeners.begin(), listeners.end(), callback);
}

Bueno bueno, usamos un lambda y la función std::for_each, pero meh, es lo mismo. Y con esto ya tenemos listo nuestro patrón “observer”, y sólo resta el usarlo: primero creamos una función que imprime el nombre de la propiedad que ha sido modificada. Luego, hacemos otra función que haga ciertas validaciones (nombre no vacío, edad mayor de 16 años, etc.) e imprima un mensaje si no se cumplen. Luego instanciamos nuestro objeto employee y añadiremos las funcinoes a la propiedad listeners.

int main(int argc, char* argv[])
{
    auto print_func = [](event_args& args) { 
        cout << "La propiedad [" 
             << args.property_name()
             << "] ha cambiado. "
             << endl;
    };

    auto validate_func = [](event_args& args) {
        auto sender = args.sender();
        if (sender.name().length() <= 0)
            cout << "El nombre no puede estar vacío." << endl;
        if (sender.age() < 16)
            cout << "La edad mínima laboral es de 16 años." <= 255)
            cout << "La dirección es muy larga." << endl;
        cout << endl;
    };

    employee emp;
    emp.listeners.push_back(print_func);
    emp.listeners.push_back(validate_func);
    emp.name("Fernando");
    emp.name("");
    emp.name("Fernando Gómez");
    emp.age(29);
    emp.address("Algún lugar en la Ciudad de México.");

    system("pause");
    return 0;
}

Al ejecutarse, este programa tiene esta salida:

La propiedad [name] ha cambiado.
La edad mφnima laboral es de 16 a±os.

La propiedad [name] ha cambiado.
El nombre no puede estar vacφo.
La edad mφnima laboral es de 16 a±os.

La propiedad [name] ha cambiado.
La edad mφnima laboral es de 16 a±os.

La propiedad [age] ha cambiado.

La propiedad [address] ha cambiado.

Press any key to continue . . .

¡Así sí gana la gente!

Hemos visto en el pasado cómo usar diferentes técnicas para llamar funciones como variables o parámetros. Y ahora hemos visto cómo podemos combinarlas, junto con esta nueva función exclusiva de C++ 11, para lograr estructuras más complejas. No es que antes no pudieran hacerse, pero ahora no tenemos que escribir tanto código. Y al final de todo es es lo que debe proveer una buena librería, ¿o no?

Por último, vale la pena comentar que esto es apenas un vistazo a std::function y, sobre todo, a todos los objetos y funciones contenidos dentro del archivo <functional>. Poco a poco iremos explorando más posibilidades, así que ¡sigue en contacto!

¡Hasta la próxima!

Boostificando mi vida


Vaya cosas de la vida. La gente que me conoce sabe que soy C++ boy desde hace años: no por nada mis sobrenombres son Fer++ o F++.

Aunque no fue el primer lenguaje que aprendí (ese fue QBASIC y luego Visual Basic 6, cuando iba en la preparatoria), fue mi primer lenguaje profesional. Antes de salir de la preparatoria ya había hecho trabajos con HTML, JavaScript y CSS, pero mi primer trabajo para una empresa de desarrollo fue como programador de C++: cuando apenas tenía 19 años en marzo de 2002. Desde ahí nunca he dejado de programar con este lenguaje.

Seguro, el boom de .NET llegó a México alrededor del 2004, y hubo altibajos, pero en general creo que fui bastante constante. En 2008 fue cuando ahora sí mi trabajo se volvió puro .NET con algo de SharePoint. Sin embargo, ahora que el código no administrado viene de vuelta, con todo, me emociona volver a usar este lenguaje y de hecho ya estoy aprendiendo Metro, la nueva API de Microsoft para Windows 8. Y no soy el único: justo hoy se está llevando a cabo la conferencia Going Native 2012.

¿Cuántos libros de C++ no habré leído con entusiasmo? The Standard C++ Library, de Nicolai Josuttis, o qué tal Modern C++ Design de Andrei Alexandrescu… He probado varias librerías: obviamente MFC, pero también la Windows Template Library, Qt o incluso wxWidgets. Y sin embargo… nunca había utilizado Boost.

Boost, para quien no sepa, es una librería, o conjunto de librerías de C++, diseñadas para integrarse bien con el estándar de C++. Proveen mucha funcionalidad que el estándar todavía no contempla, y éstas son revisadas por mucha gente de la comunidad, incluídos participantes del comité de estandarización de C++. Es más, muchos componentes de Boost suelen terminar en el estándar. Pero no dejes que te lo cuente, entra a este enlace y entérate por tí mismo.

Ahora, no es que no supiera de la existencia de Boost. Hace algunos años intenté instalarlo pero la verdad que fue un poco engorroso el construirlo. La mayoría de los componentes de Boost son puros encabezados, perohay ciertaspartes que requieren enlazar con librerías estáticas, y ahí es donde estaba el problema.

Y sucede que con todo este fervor por regresar al mundo nativo, en parte promovido por Microsoft y su Windows 8, ahora sí decidí echarle un ojo. Me encontré conque ya tenían un instalador, así que ésta fue bien fácil. Sólo tuve que agregar el directorio de instalación a

image

Y ahora sí, estamos listos para usar Boost. Probemos algo sencillo.


#include <iostream>
#include <boost/array.hpp>

int main(int argc, char* argv[])
{
    boost::array a;
    for (auto i = 0; i < a.size(); i++)
        a[i] = i * i;

    for (auto i = a.begin(); i != a.end(); ++i)
        std::cout << *i << std::endl;

    system("pause");
    return 0;
}

 

La clase boost::array en realidad es un simple envoltorio de un array natural, pero provee ciertas características adicionales como el contar con iteradores begin() y end(). En fin, no importa. Como estas clases, Boost cuenta con muchísimas más.

Y he de decir que hasta donde he podido explorar he encontrado muchas clases muuuuuuucho muy útiles. De hecho poco a poco las iré exponiendo por aquí. Pero venga, anímate tú también y boostifica tu vida.

¡Nos vemos!

Categorías:Boost, C++, Noticias Etiquetas: ,

Argumentos variables


Una de las herencias de C que fue bien acogida en C++ es el hecho de que una función tenga argumentos variables. Es decir, que el número de parámetros pueda variar y la función pueda reaccionar a este hecho como es debido.

Veamos un poquito de teoría. Todas las funciones, métodos, etc., tienen una dirección en memoria. El tamaño depende de la firma y el tipo de dato de regreso. La firma es el número de parámetros. Así, int foo(int a, int b); guardaría sizeof(int) bytes para el valor de retorno, el doble para los parámetros y otro tanto para la propia dirección de la función. De esta forma, los parámetros se albergan de forma contínua en memoria, al igual que un array. Entonces, conociendo yo la dirección en memoria de el primer parámetro, es fácil obtener el segundo:

int foo(int a, int b)
{
    int* p1;
    int* p2;

    p1 = &a;
    p2 = p1 + sizeof(int);
    cout << "Valor del segundo parámetro: " << *p2 << endl;
}

Este hecho es lo que hace posible que una función pueda tener parámetros variables: basta con que obtenga la dirección en memoria de la primera función para que, conociendo el tipo de parámetros que me envían, pueda convertir los bytes en el tipo de dato correspondiente.

Ok, basta de teoría. La librería estándar de C++ contiene –heredada de C– una serie de funciones que automatizan la tarea de manejar direcciones de memoria, sumarlas, etc. Veamos una pequeña referencia del tipo de dato y las funciones mencionadas.

  • va_list. Este tipo de dato va a albergar el puntero a las direcciones de memoria que contienen los argumentos. Usualmente es un typedef de un char*, ya que en esencia su utilidad es apuntar adecuadamente al búfer.
  • va_start. Esta función inicializa nuestra variable de tipo va_list. Toma dos parámetros. El primero es nuestra variable de tipo va_list. El segundo es el último parámetro conocido. Lo que hace esta función (o macro) es lo antes mencionado, toma la dirección de memoria del último parámetro conocido y posiciona el búfer pasada ésta dirección.
  • va_arg. Nos devuelve el valor del siguiente parámetro variable. Toma dos parámetros, el primero es nuestra variable va_arg, y el segundo es el tipo de dato que vamos a convertir (es decir, si el parámetro es un int, un double, etc; esto, para mover correctamente el puntero va_list y hacer el cast correspondiente).
  • va_end. Una vez que terminamos, hay que emplear esta macro para indicar que hemos terminado. El único parámetro es nuestro puntero va_list.

Es importante recalcar que es necesario saber el número de parámetros que se van a intentar obtener, de alguna forma, así como su tipo de dato. Por ejemplo, printfobtiene esta información del número de % que encuentre, así como el tipo de dato especificado (%d, %c, %s, etc). La función va_arg se puede llamar tantas veces como se requiera, pero hay que tener en cuenta que si nos pasamos podemos provocar algún comportamiento indefinido.

Bueno, en vivo y a todo color, un ejemplo.

int Suma(int cuenta, ...)
{
    va_list args;
    int total;

    va_start(args, cuenta);
    total = 0;

    for (int i = 0; i < cuenta; i++)
    {
        total += va_args(args, int);
    }
    va_end(args);

    return total;
}

int main()
{
    cout << "Suma 1+3+5+7 = " << Suma(4, 1, 3, 5, 7) << endl;
    cout << "Suma 1+2+3+4+5+6+7+8+9+10 = " <<
        Suma(10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10) << endl;
    cout << "Suma 6+9+8+4+3+0 = " << Suma(6, 6, 9, 8, 4, 3, 0) 
        << endl;

    return EXIT_SUCCESS;
}

Ahora solo queda que juegues con esto y hagas funciones más complicadas.

Categorías:Apunte, C++, Independiente Etiquetas:

Polimorfismo genérico


Si ya tienes tiempo trabajando con programación orientada a objetos, habrás notado cuán útil es el polimorfismo. Un ejemplo muy útil de esto es el patrón de diseño de la estrategia. Pero hay muchos otros ejemplos. Gracias a esto podemos cambiar comportamientos, y bueno, todo lo chido que tenemos con el polimorfismo.

Ahora bien, también tenemos una forma de hacer una especie de polimorfismo genérico, que me gustaría comentar dado que algunas librerías (como ATL o WTL) lo emplean. Consideremos el siguiente ejemplo:

template<class T>
class Base
{
public: 
    void Foo() 
    {
        T* pT = static_cast<T*>(this);
        pT->Imprimir();
    }
 
    void Imprimir() { cout << "Base::Imprimir"; }
};
 
class Derivada1 : public Base<Derivada1>
{
    // no hay nada aquí
};
 
class Derivada2: public Base<Derivada2>
{
    void Imprimir() { cout << "Derivada2::Imprimir"; }
};
 
int main()
{
    Derivada1 d1;
    Derivada2 d2;
 
    d1.Foo();    // imprime: "Base::Imprimir
    d2.Foo();    // imrpime: "Derivada2::Imprimir
 
    return 0;
}

Interesante, ¿no? Quizás hasta desconcertante. La clase Base toma un parámetro de plantilla que, además, espera sea alguna derivada de la misma Base. Lo interesante se presenta en la función Foo. Marcado en negritas, está la siguiente llamada: T* pT = static_cast<T*>(this);. ¿Qué quiere decir? Que vamos a convertir nuestro puntero (this) a una de las clases derivadas. Esto es correcto, toda vez que estamos pensando que el parámetro de plantilla sea una derivada de Base. Entonces, al hacer un static_cast hacia el parámetro de plantilla, e invoca a la función Imprimir de la misma; dado que no es virtual, no se invocará la implementación de la clase derivada, sino la implementación del parámetro de plantilla. Por eso es necesario hacer el static_cast. Así, al convertir a Derivada1, como ésta no tiene una implementación de Imprimir queoculte al Imprimir de Base, entonces se manda llamar a la versión de ésta. Sin embargo, Derivada2 sí implementaImprimir, y por lo tanto, oculta la de Base, y por ende es ésta la versión que es llamada.

Es interesante esta técnica. Tiene algunas ventajas, como el hecho de salvar memoria dado que no hay necesidad de vtables (tablas virtuales), o que todas las llamadas se resuelven en tiempo de compilación, por lo que pueden ser (y son) optimizadas. Pero quizás lo más importante es que es polimorfismo, pero no hay necesidad de tener que declarar una función virtual. Esto es muy importante en diversos escenarios, cuando, digamos, tenemos una librería que implementa el método X y que nosotros quisiéramos sobreescribir: dicho método tendría que haber sido declarado como virtual. Si no lo fué, aún tenemos la posibilidad de emplear el polimorfismo genérico.

Esta es, quizás, la característica más importante de esto.

Categorías:Apunte, C++, Independiente Etiquetas: ,

Estrategias


Supongamos que tenemos un conjunto de datos en particular, y que para manipularlos contamos con diferentes algoritmos para llevarlo a cabo. Ahora supongamos que por motivos de la vida tenemos que seleccionar el algoritmo a emplear en tiempo de ejecución (digamos tras seleccionar alguna opción o bajo determinadas circunstancias).

Bajo este escenario, tenemos que existe el patrón de diseño "Estrategia" (Strategy design pattern), que precísamente se centra en resolver este problema. Es decir, su finalidad es proveer un mecanismo para definir una familia de algoritmos, encapsularlos como objetos y hacerlos intercambiables. El patrón estrategia permite variar los algoritmos independientemente de los clientes que los usen. Esto se logra al desacoplar el algoritmo de su huésped (es decir, el cliente que lo utilizará), separándolos en otra clase.

Las ventajas de hacer esto son varias. En primera instancia, si tienes diferentes comportamientos es más fácil llevar el rastro de éstos en clases separadas en lugar de que todo esté en un mismo método, separado por ifs y elses. Otra ventaja es que si posteriormente se requiere agregar (o eliminar o cambiar) un comportamiento (i.e. el algoritmo), bastará con crear (o modificar) una clase derivada y pasarla a la clase ejecutora, y listo. Que por cierto, cada "comportamiento" o "algoritmo" es llamado una "estrategia", de ahí el nombre. Otra ventaja, inherente, es que las "estrategias" no están acopladas con el "contexto".

¿Cuándo emplear este patrón? Un buen criterio es cuando tienes varios objetos que son esencialmente lo mismo, pero que solo difieren en su comportamiento. De esta forma, se reducen todos estos objetos diferentes a uno solo que usa varias estrategias. Este empleo también es una buena alternativa al subclaseo de un objeto para alcanzar diferentes comportamientos. Normalmente, la idea de subclasear un objeto se pretende cambiar su comportamiento, que en principio es estático (a diferencia de cuando se pretende cambiar lo que hace, en cuyo caso sería mejor derivar esa clase y a través del polimorfiso, redefinir los métodos necesarios). Con las estrategias, todo lo que se necesita es reemplazar el objeto con una diferente estrategia, y listo. Es decir, si en algún momento de la vida tienes una clase que subclasea a otra… bien, podrías ir pensando en hacer una refactorización e implementar el patrón estrategia.

Pero bueno, ya fue mucha teoría, veamos algo de código. En primera instancia, el modelo a emplear para implementar este patrón sería algo similar a lo que muestra este diagrama (el cual es un fusil de la Wikipedia).

strategypattern

La clase Context es en esencia el objeto que mandará llamar a la estrategia definida. Y lo hará a través de una referencia a la clase base que define la estrategia propia. Posteriormente, se derivan tantas clases como estrategias se necesiten y listo. La clase del contexto deberá, por supuesto, tener una referencia a la estrategia que deberá seguir, y ponerlo en el constructor sería lo más adecuado.

Pero antes de ver código, pongamos un escenario práctico. El primero que viene a mi menete, y que es sencillo de entender, es el siguiente. Supongamos que tenemos una clase que tiene como miembro un array de, er, lo que sea, digamos cadenas de texo. Y lo que queremos es determinar una forma de ordenar dicho array. Dependiendo de cuántos elementos tenga el array, querremos ordenarlo por el método del quick sort, el método de la burbuja y quizás el método de selección. Entonces tendríamos que nuestro objeto "contexto" sería uno que tuviera un array; la estrategia "base" debería tener un método "Sort" donde le pasamos como parámetro el array a ordenar, y finalmente derivaríamos tres clases: QuickSort, BubbleSort e InsertionSort.

Tonz codiguillo:

class SortStrategy
{
    public:
        SortStrategy() { }
        virtual ~SortStrategy() { }

        virtual void Sort(string* array, int size) = 0; // virtualmente pura
};

class QuickSortStrategy : public SortStrategy
{
    public:
        QuickSortStrategy() { }
        virtual ~QuickSortStrategy() { }

        virtual void Sort(string* array, int size)
        {
            QuickSort(array, size-1, 0);
        }

    private:
        void QuickSort(string* array, int top, int bottom)
        {
            int middle;
        
            if (top < bottom)
            {
                middle = Partition(array, top, bottom);
                QuickSort(array, top, middle); //sí, estamos empleando recursión
                QuickSort(array, middle+1, bottom); 
            }
        }

        int Partition(string* array, int top, int bottom)
        {
            string x = array[top];
            int i = top - 1;
            int j = bottom + 1;
            string temp;

            do
            {
                do { j--; } while ( x.compare(array[j]) > 0);
                do { i++; } while ( x.compare(array[i]) < 0);

                if (i < j)
                { 
                    temp = array[i];
                    array[i] = array[j];
                    array[j] = temp;
                }
            } while (i < j);    
     
            return j;           
        }
};

class BubbleSortStrategy : public SortStrategy
{
    public:
        BubbleSortStrategy() { }
        virtual ~BubbleSortStrategy() { }

        virtual void Sort(string* array, int size)
        {
            // implementamos el bubble sort
            int i;
            int j;
            string aux_elem;
            int movs;

            movs = 0;

            for (int i = 0; i < size - 1; i++)
            {
                for (j = 1; j < size; j++)
                {
                    if (array[j] < array[j-1])
                    {   
                        aux_elem = array[j];
                        array[j] = array[j-1];
                        array[j-1] = aux_elem;
                        movs++;
                    }
                }
            }
        }
};

class InsertionSortStrategy : public SortStrategy
{
    public:
        InsertionSortStrategy () { }
        virtual ~InsertionSortStrategy () { }

        virtual void Sort(string* array, int size)
        {
            // implementamos el insertion sort
            int i, j, min;
            string str;

            for (i = 1; i > size; i++)
            {
                min = i;
                for (j = i+1; j <= n; j++)
                {
                    if (array[j] < array[min])
                        min = j;
                }

                str = array[min];
                array[min] = i;
                array[i] = str;
            }
        }
};

class Names
{
    private:
        string* _names;
        int _size;
        SortStrategy* _strategy;

    public:
        Names(int size, SortStrategy* strategy) 
        { 
            _size = size; _names = new string[size]; 
            _strategy = strategy; 
        }

        ~Names() { delete [] _names; delete _strategy; }

        inline int GetSize() const { return _size; }
        string& GetAt(int index) { return _names[index]; }
        string& operator[] (int index) { return GetAt(index); }

        void PrintNames(ostream& stream)
        { 
            _strategy->Sort(_names, _size);
            for (int i = 0; i < _size; i++) 
                stream << i << _names[i] << endl;
        } 
};

Ahora solo tenemos que usar las clases. En el ejemplo, pediremos que nos diga el usuario si ordenar por quick sort, por burbuja o por inserción. En situaciones más reales, digamos que decidiríamos de acuerdo al número de elementos que vamos a ingresar, pero en aras de hacer la cosa más sencilla, lo haremos así.

int main()
{
    SortStrategy* strategy;
    int opt;

    cout << "Seleccione una opción" << endl;
    cout << "1. QuickSort" << endl;
    cout << "2. Método de la burbuja" << endl;
    cout << "3. Método de inserción" << endl;
    cin >> opt;

    switch (opt)
    {
        case 1:
            strategy = new QuickSortStrategy(); break;

        case 2:
            strategy = new BubbleSortStrategy(); break;

        case 3:
            strategy = new InsertionSortStrategy(); break;

        default:
            cout << "Opción inválida, se usará QuickSort." << endl;
            strategy = new QuickSort();
            break;
    }

    Names names = strategy; // jeje, constructor implícito llamado aquí
    names[0] = "Fernando";
    names[1] = "Catalina";
    names[2] = "Eduardo";
    names[3] = "Tania";
    names[4] = "Andrés";
    names.PrintNames(cout);

    return EXIT_SUCCESS;
}

En cualquier caso, el código anterior nos mostraría la siguiente salida:

Andrés
Catalina
Eduardo
Fernando
Tania

ejemplificando a la perfección lo que comentaba arriba, de que el comportamiento no cambia, sino simplemente la forma de llevarlo a cabo. Por cierto que, si eres avispado, te habrás dado cuenta de que una cosa buena de esto es que las clases de estrategia (en este caso, QuickSortStrategy, BubbleSortStrategy y InsertionStrategy) pueden ser empleadas en cualquier lugar que quieras. Esto es a lo que nos referíamos con el hecho de que no están acopladas. Entonces una clase de búsqueda cualquiera, siempre que siga un patrón determinado (en este caso, la implementación del método Sort) puede emplearse como estrategia de búsquedas.

Bueno, pues farewell. Espero que te sirva de algo todo esto, de verdad que es uno de los patrones de diseño más empleados.

Tschüss!

Categorías:Apunte, C++, Independiente Etiquetas: ,

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: ,