Inicio > Apunte, C++, Independiente > Estrategias

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: ,
  1. Aún no hay comentarios.
  1. No trackbacks yet.

Responder

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión / Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión / Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión / Cambiar )

Google+ photo

Estás comentando usando tu cuenta de Google+. Cerrar sesión / Cambiar )

Conectando a %s