Archivo

Posts Tagged ‘Diseño’

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!

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

Clases singleton


A lo largo de todo el tiempo que C++ lleva de existencia, han surgido muchos métodos de diseño. Al principio, era una mezcla rara de C con C++. Posteriormente y conforme la orientación a objetos iba ganando adeptos, surgieron nuevas técnicas. En particular, los llamados "patrones de diseño". Estos son simples directivas para crear software –clases por lo general– que resuelven ciertos problemas particulares. Cómo y cuándo aplicar un patrón es el objetivo.

Quizás el patrón de diseño más sencillo es la clase llamada singleton. En esencia, una clase singleton es una que garantiza que solo podrá haber una instancia de ésta durante toda la ejecución de la aplicación. Ese de hecho es el patrón: una instancia, no más.

¿Para qué nos sirve un singleton? En esencia, es como si fuera una variable global. Una instancia solamente, accesible siempre. Pero a diferencia de una variable global, es orientada a objetos, lo cuál implica que podemos sacar ventaja a –por ejemplo– la encapsulación. En lugar de acceder directamente a la variable, lo hacemos a través de los métodos que ofrece la clase. Pero… también podríamos crear una clase que encapsule el valor, y tener una instancia de ésta en una variable global. ¿De qué nos sirve, entonces, el condenado singleton?

Un ejemplo, el más común para emplear un singleton: un archivo de configuración de nuestra aplicación. Ciertamente en uno de estos casos, la información tiene que ser accesible a toda la aplicación. Pero pensemos. Supongamos que tenemos un proceso que hace uso de esa información. Corremos el proceso y después cambiamos la configuración del archivo. Al tener varias instancias, tendríamos que asegurarnos de que nuestro proceso vuelva a leer dichos valores, lo cuál es una pérdida de tiempo. Pero para eso tenemos nuestra variable global, ¿cierto? Basta con acceder a dicha variable global.

Pero supongamos ahora que nuestro proceso corre en un hilo separado. Entonces sí estamos en problemas, porque podríamos tener desincronizada nuestra variable. Y en este caso, ni siquiera el empleo de volatile nos salvaría. Más aún, si hay varias instancias de la clase los valores quedarían completamente desincronizados, pudiendo ser que un proceso tenga valores totalmente distintos a otro proceso. Mala cosa.

Aquí es donde entra nuestro singleton al rescate. Por lo antes expuesto, nos aseguramos de que solo haya una instancia. Y aquí mismo podríamos controlar la sincronización entre hilos con suma facilidad.

Otra razón más se me ocurre. Imaginemos que la configuración del sistema no sale de un archivo, sino a través de un procedimiento más complicado, como conectarse a una base de datos remota, a otro sistema (un ERP como SAP) o a un servicio web. Cada vez que obtuviésemos los datos gastaríamos tiempo de proceso y memoria. Con el singleton ya no sería necesario más que una sola vez para obtener los datos, y otra para guardarlos. Más aún, ¿qué pasa en el caso de que bajo ciertas condiciones, la aplicación no necesitase leer dicho archivo? ¡Estaríamos leyéndolo a lo güey! otra de las características de un singleton es que se instancia solamente la primera vez que se necesita. Problema resuelto.

Si no te convencen las razones para emplear el singleton, puedes suspender la lectura en este punto e ir a ver algún otro artículo. Si sí, a continuación presento cómo sería el singleton. Siguiendo el ejemplo de nuestro archivo de configuración, presento una clase singleton de este tipo.

class configuracion
{
    private:
        string _val1;
        string _val2;
        string _val3;

        configuracion()
        {
            cargar(); 
        }

    public:
        ~configuracion()
        {
            guardar();
        }

        void cargar()
        {
            // lee la configuración desde el archivo
        }

        void guardar()
        {
            // guarda la configuración al archivo
        }

        string get_val1() const { ... }
        string get_val2() const { ... }
        string get_val3() const { ... }
        void set_val1(const string&amp; val1) { ... }
        void set_val2(const string&amp; val2) { ... }
        void set_val3(const string&amp; val3) { ... }

        static configuracion&amp; instancia()
        {
            static configuracion config;
            return config;
        }
};

Lo primero que notarás es que el constructor es privado. En efecto, de esta forma nos aseguramos que nadie pueda crear una instancia más que la clase misma. Por lo mismo, tenemos que crear un método público y eventualmente estático que nos genere la única instancia que va a haber corriendo en la aplicación. ¿Cómo lo consigue? Sencillo. Creamos una variable estática dentro del método. Esta instancia se iniciará la primera vez que se llame al método estático, con lo cuál cumplimos otro de los requisitos. Y al ser una referencia, nos aseguramos de que el objeto se destruye al finalizar la aplicación. Así, su utilización sería de la siguiente forma:

configuracion&amp; config = configuracion::instancia();
string val2 = config.get_val2();
config.set_val1(&quot;hola&quot;);
config.guardar(); // si queremos guardar en este momento, claro está

Una cuestión. Notarás que instancia() regresa una referencia. ¿Por qué no un puntero? Sería más adecuado, y menos engorroso. Posiblemente. Pero entonces algún novato podría hacer lo siguiente:

configuración* pconfig = configuracion::instancia();
delete pconfig;

Sobra decir que estamos fritos. Entonces, por cuestiones de seguridad, mejor regresamos una referencia, ya que hacer un delete sobre la referencia produciría un error en tiempo de compilación.

Por supuesto, hay varias formas de realizar un singleton. He presentado la más común. Podrías emplear asignación dinámica de memoria y regresar una referencia para evitar el problema anterior. En cuyo caso tendrías el problema de en qué momento hacer el delete del objeto. Lo podrías poner en el destructor, pero te arriesgas a un comportamiento indefinido. O podrías hacer un "envoltorio" estilo el std:.auto_ptr. Creo yo, sin embargo, que esta es la forma más sensata y la más usual. De emplear otra, deberías tener buenas razones para ello. Y con suerte, estas otras formas las trataremos en otros temas. Mientras, ya sabes qué es un singleton, para qué sirve, y cómo emplearlo.

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