Archivo

Archive for 30 noviembre 2010

Todo lo que siempre quisiste saber sobre colecciones y tenías miedo de preguntar… Tres diccionarios especializados

noviembre 30, 2010 1 comentario

Hasta el momento, la serie de artículos dedicados a colecciones solo ha presentado un simple diccionario: Hashtable. Como habíamos visto antes, éste diccionario agrupa los pares llave-valor basándose en el hash de la llave. Ahora bien, este tipo de ordenamientos suele ser eficiente, sobre todo cuando tenemos diccionarios enormes. Pero hay situaciones en las que Hashtable no es tan eficiente, o bien bajo ciertas circunstancias algun enfoque diferente puede ser mejor. En esta entrada vamos a analizar tres diccionarios que se encuentran dentro del espacio de nombres System.Collections.Specialized, y que ofrecen dichos enfoques para escenarios muy específicos: ListDictonary, HybridDictionary y OrderedDictionary.

Comencemos por estudiar ListDictionary. A primera vista, ListDictionary luce como una implementación normal de IDictionary, y de hecho así es. No añade métodos nuevos. Cuenta con los métodos y propiedades que implementan ICollection, como Count, IsSynchronized, SyncRoot, CopyTo; y también con propiedades y métodos propios de un IDictionary, como IsFixedSize, IsReadOnly, Keys, Values, Add, Clear, Contains, Remove y el indexador para acceder a los valores dada una llave; todos estos miembros que ya hemos explicado en entradas anteriores. De hecho, incluso contiene menos métodos que Hashtable. Entonces la pregunta es: ¿para qué nos sirve ListDictionary?

La respuesta es que mientras Hashtable almacena los pares llave-valor en bloques de memoria que son accesibles vía el código hash de la llave, ListDictionary los almacena como si fuera una lista enlazada. Es decir, el primer elemento contiene el par llave-valor, más un apuntador (o referencia, en el argot de C#) al elemento siguiente, y así sucesivamente hasta llegar al final de la lista. Esto implica que ListDictionary no manipula la memoria (o al menos, no de la forma en que Hashtable lo hace) y por lo tanto el acceso a los elementos es más eficiente en términos de memoria y procesamiento.

Sin embargo esto tiene su precio. Si recuerdas tus viejas clases de estructuras de datos que llevaste en la universidad, sabrás entonces que una lista enlazada tiene un mal rendimiento cuando intentamos acceder a los elementos de forma secuencial. De hecho, esta es la diferencia más grande que hay entre un array o vector (donde los elementos se almacenan en bloques de memoria contigua, haciendo posible el acceso secuencial) y la lista enlazada, donde se comienza a navegar por la cabeza de la lista, yendo de uno en uno hasta localizar el elemento que nos interesa. Luego entonces, el rendimiento de una lista (y por ende, de ListDictionary) al querer acceder a un elemento disminuye considerablemente conforme el número de elementos contenidos aumenta.

Para probar lo anterior, consideremos el siguiente código.

using System;
using System.Collections;
using System.Collections.Specialized;

namespace Blogoso
{
    class Program
    {
        static void Main(string[] args)
        {
            const int elements = 10;

            IDictionary dic = new ListDictionary();
            for (int i = 0; i < elements; i++)
            {
                string key = i.ToString();
                string val = string.Format("Cadena {0}", i);
                dic.Add(key, val);
            }

            DateTime start = DateTime.Now;
            for (int i = 0; i < elements; i++)
            {
                var key = i.ToString();
                var val = dic[key];
                Console.WriteLine("{0}: {1}", key, val);
            }

            TimeSpan duration = DateTime.Now - start;
            Console.WriteLine("Duración: {0}", duration);

            Console.ReadKey(true);
        }
    }
}

En este pequeño programita, creamos un ListDictionary (línea 13), le añadimos un número determinado de elementos (especificado en la constante definida en la línea 11) y posteriormente recorremos todo el diccionario y lo mostramos en la consola. Para hacer el ejemplo más interesante, tomamos el tiempo de inicio (línea 21) y el tiempo final (línea 29) y mostramos el tiempo que tardó el diccionario en obtener los valores. Si ejecutamos el programa, obtendríamos un resultado similar al siguiente (puede variar dependiendo de las características de la máquina que lo ejecute).

0: Cadena 0
1: Cadena 1
2: Cadena 2
3: Cadena 3
4: Cadena 4
5: Cadena 5
6: Cadena 6
7: Cadena 7
8: Cadena 8
9: Cadena 9
Duración: 00:00:00.0020001

Después de ejecutar el programa en repetidas ocasiones, el tiempo empleado suele estar entre 2 y 3 milésimas de segundo. Ahora bien, si en lugar de diez elementos cambiamos la línea 11 para que sean mil, el tiempo de ejecución me aparece entre 186 y 199 milésimas de segundo. Para comparar, cambiemos la línea 13 para que en lugar de un ListDictionary creemos un Hashtable:

IDictionary dic = new Hashtable();

Si añadimos diez elementos, el tiempo promedio se encuentra entre 3 y 4 milésimas de segundo, un pequeño aumento con respecto a ListDictionary. Sin embargo, al añadir mil elementos, obtengo un tiempo promedio de entre los 155 y 170 milésimas. Una baja sensible con respecto a ListDictionary. Repitiendo el ejercicio pero ahora con 10,000 elementos, tenemos que mientras Hashtable me da tiempos de entre 800 milésimas y 1.06 segundos, ListDictionary me da tiempos de entre 2.9 y 3.2 segundos.

Este pequeño experimento nos permite concluir que ListDictionary es más eficiente que Hashtable cuando nuestro conjunto de datos es pequeño, pero que conforme dicho conjunto crece, el rendimiento comienza a bajar considerablemente. De hecho, el último ejercicio con diez mil elementos mostró que el rendimiento de Hashtable con respecto a ListDictionary puede ser entre 200% y 300% mayor.

Luego entonces tenemos la premisa de ListDictionary: es un diccionario optimizado para trabajar con pocos elementos (la documentación dice que no más de diez), de tal suerte que si el número de elementos es grande, es preferible utilizar otro tipo de diccionario, pero que si el número permanece bajo se obtendrán mejores resultados con ListDictionary.

Y esto precísamente nos lleva al segundo diccionario especializado a tratar. Supongamos que tenemos un escenario en el cual tenemos un diccionario que generalmente tiene pocos elementos. Pero que bajo ciertas circunstancias, éste puede crecer. Para no perder el rendimiento, tendríamos que usar ListDictionary y cuando éste crezca, copiar todos los elementos a un Hashtable. Pues bien, hay una colección que hace precísamente eso: HybridDictionary. Esta clase emplea de forma interna un ListDictionary cuando el número de elementos se mantiene bajo, pero que cambia a Hashtable cuando el número crece.

Al igual que ListDictionary, HybridDictionary implementa las mismas propiedades y métodos que definen ICollection e IDictionary, con la salvedad que el constructor nos permite especificar el número de elementos que contendrá la colección de forma inicial. Pero evidentemente la fortaleza de la clase radica en las estrategias utilizadas para optimizar el acceso a los pares llave-valor. Para mostrar esto, tomemos el ejercicio que habíamos practicado anteriormente y modifiquémoslo para utilizar HybridDictionary.

using System;
using System.Collections;
using System.Collections.Specialized;

namespace Blogoso
{
    class Program
    {
        static void Main(string[] args)
        {
            const int elements = 10;

            IDictionary dic = new HybridDictionary();
            for (int i = 0; i < elements; i++)
            {
                string key = i.ToString();
                string val = string.Format("Cadena {0}", i);
                dic.Add(key, val);
            }

            DateTime start = DateTime.Now;
            for (int i = 0; i < elements; i++)
            {
                var key = i.ToString();
                var val = dic[key];
                Console.WriteLine("{0}: {1}", key, val);
            }

            TimeSpan duration = DateTime.Now - start;
            Console.WriteLine("Duración: {0}", duration);

            Console.ReadKey(true);
        }
    }
}

Cuando ejecuto en repetidas ocasiones este código, obtengo un tiempo promedio de entre 2 y 3 milésimas, similar al que obteníamos usando ListDictionary. Pero si cambiamos el número de elementos de diez a diez mil, en lugar de obtener el promedio de entre 2.9 y 3.2 segundos, obtenemos un promedio de 0.88 a 0.92 segundos, similar al que habíamos obtenido con Hashtable. Esto es así, en efecto, debido al comportamiento interno de HybridDictionary.

El tercer diccionario especializado que vamos a tratar, a diferencia de los dos anteriores, no se distingue de Hashtable o algún otro diccionario por la optimización hecha en base al número de elementos que contenga, sino más bien porque permite el acceso aleatorio a los elementos: es decir, a través de un índice. La clase se llama OrderedDictionary y, como su nombre lo sugiere, es un diccionario que mantiene ordenados los elementos basados en la llave de los mismos en bloques de memoria contigua, de forma muy similar a como los almacena un array o un vector. La principal ventaja de esto es que podemos acceder a los elementos no solamente a través de la llave, sino también a través de un índice.

using System;
using System.Collections;
using System.Collections.Specialized;

namespace Blogoso
{
    class Program
    {
        static void Main(string[] args)
        {
            OrderedDictionary dic = new OrderedDictionary();
            dic.Add("John", "Imagine");
            dic.Add("Ringo", "Photograph");
            dic.Add("Paul", "Another day");
            dic.Add("George", "My sweet lord");

            for (int i = 0; i < dic.Count; i++)
            {
                Console.WriteLine("{0} - {1}", i, dic[i]);
            }

            Console.ReadKey(true);
        }
    }
}

El ejemplo anterior muestra cómo podemos usar el indexador de OrderedDictionary para acceder por medio del índice. Esto es algo que no podemos lograr con Hashtable, y nos proporciona la comodidad de poder iterar sobre los elementos sin tener que conocer las llaves de antemano.

Un aspecto importante de esta clase es que, como hemos mencionado, internamente mantiene ordenados los elementos. Por lo mismo, es necesario poder contar con una forma de controlar el cómo comparar elementos, y es por ello que el constructor de OrderedDictionary admite una interfaz de tipo IEqualityComparer. Esta interfaz nos permite comparar dos objetos, además de obtener un código hash para alguno de ellos. A través de esto, podemos controlar cómo la colección ordena sus elementos. Por ejemplo, si las llaves son cadenas de texto (como en el ejemplo) podríamos crear una clase que implemente dicha interfaz para que las comparaciones se hicieran sin distinguir entre mayúsculas y minúsculas. Por ejemplo:

using System;
using System.Collections;
using System.Collections.Specialized;

namespace Blogoso
{
    class StringComparer : IEqualityComparer
    {
        public StringComparer()
        {
        }

        public bool Equals(object x, object y)
        {
            string xstr = x as string;
            string ystr = y as string;

            return xstr.Equals(ystr, StringComparison.InvariantCultureIgnoreCase);
        }

        public int GetHashCode(object obj)
        {
            string str = obj as string;
            str = str.ToUpper();

            return str.GetHashCode();
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            StringComparer comparer = new StringComparer();
            OrderedDictionary dic = new OrderedDictionary(comparer);
            dic.Add("John", "Imagine");
            dic.Add("Ringo", "Photograph");
            dic.Add("Paul", "Another day");
            dic.Add("George", "My sweet lord");

            for (int i = 0; i < dic.Count; i++)
            {
                Console.WriteLine("{0} - {1}", i, dic[i]);
            }

            Console.ReadKey(true);
        }
    }
}

La clase StringComparer que creamos compara dos cadenas de texto sin distinguir entre mayúsculas y minúsculas (método Equals), mientras que GetHashCode regresa el código hash de la cadena de texto en mayúsculas, de tal suerte que “John”, “JOHN” y “john” regresen el mismo código. Y ahora sí, inicializamos nuestro OrderedDictionary con una instancia de este comparador.

A lo largo de esta entrada analizamos los tres diccionarios especializados que nos ofrece .NET, cada uno con sus ventajas, virtudes y desventajas. Vimos cómo ListDictionary y HybridDictionary nos pueden ayudar a optimizar nuestro código, mientras que OrderedDictionary nos permite acceder a valores mediante un índice, sin necesidad de conocer las llaves de antemano. Así las cosas, la regla a seguir pudiera expresarse así. Si un diccionario tiene un número bajo de elementos, ListDictionary es la opción con mejor rendimiento. Si el diccionario tiene un número inicial bajo de elementos, pero es posible que éste crezca, la mejor opción es utilizar HybridDictionary. Por otra parte, si el número de elementos inicial es alto, es mejor utilizar Hashtable desde un principio. Finalmente, si tenemos un diccionario cuyas llaves no son conocidas, o en algún punto no se tiene alguna forma de inferirlas, podemos optar por emplear OrderedDictionary, que nos provee esta funcionalidad.

Después de leer esta entrada, pudieras pensar que a final de cuentas los tres diccionarios especializados son una pérdida de tiempo. Al fin y al cabo, Hashtable puede hacer todo el trabajo, y quizás una diferencia de dos segundos no parezca significativa para un programa de negocios tradicional. Pero estas tres clases muestran la riqueza del .NET Framework, dado que proveen opciones para escenarios muy específicos. Y ciertamente, vale la pena, creo, conocerlos para poder sacarles provecho cuando nos encontremos en una situación similar.

Ahora que por supuesto, estos tres diccionarios no son los únicos que hay. En entradas futuras veremos otros tipos de diccionarios diseñados para otras situaciones específicas, pero también algunos de propósito general. No te pierdas las siguientes entradas en la serie, para conocer más sobre este fascinante y amplio tema de las colecciones en .NET.

Categorías:.NET Framework, C#, Tutorial Etiquetas:

Todo lo que siempre quisiste saber sobre colecciones y tenías miedo de preguntar… Algunas colecciones para trabajar con texto


A lo largo de esta serie “Todo lo que siempre quisiste saber sobre colecciones y tenías miedo de preguntar” hemos tratado con implementaciones de colecciones y diccionarios que vieron la luz del día en la primera versión del .NET Framework, cuando todavía no había clases genéricas. En aquella época, la norma era que cuando se necesitaba una colección fuertemente tipada el programador creaba una clase que implementara ICollection o IDictionary, internamente utilizara un ArrayList o un Hashtable, y actuara como envoltorio sobre ésta. Por supuesto, con la llegada de Generics en .NET 2.0, ésta práctica comenzó a caer en desuso (por ejemplo, ahora es más común derivar de List<T>, de Collection<T> o Dictionary<TKey, TValue>, pero todavía falta para que veamos Generics en esta serie).

Sin embargo, los propios programadores de .NET crearon una serie de clases para este escenario, sobre todo que utilizaran mucho. Por ejemplo, la clase System.Web.HttpRequest tiene una propiedad llamada QueryString, que se utiliza para acceder a un diccionario en donde la llave y el valor son ambos cadenas de texto.

Así, en este apunte vamos a explorar algunas de las clases que podemos utilizar, en particular, para trabajar con cadenas de texto: StringCollection, StringDictionary y NameValueCollection, todas dentro del espacio de nombres System.Collections.Specialized.

La primera clase es StringCollection. Como seguramente habrás inferido, ésta representa una colección de cadenas de texto, y está particularmente optimizada para trabajar con cadenas de texto. Recordemos que en .NET las cadenas de texto son inmutables, por lo que el empleo de éstas debe realizarse de forma cuidadosa para evitar caer en problemas de rendimiento.

Como cabría esperar, la colección cuenta con varias propiedades estándares, como SyncRoot e IsSynchronized para temas de concurrencia y sincronización en el acceso a elementos; Count para obtener el número de elementos que tiene la colección; IsReadOnly para saber si podemos agregar o eliminar elementos, y finalmente un indexador, a través del cuál podemos acceder a las cadenas de texto dado un índice determinado.

Además, también cuenta con los métodos tradicionales: Add y Remove para añadir o eliminar una cadena determinada; Insert para añadir una cadena en una determinada posición; AddRange para añadir un array de cadenas de texto de un solo golpe, y Clear para remover todas las cadenas existentes; CopyTo para copiar el contenido de la colección a un array unidimensional e IndexOf para obtener el índice de un elemento determinado (o –1 si no encuentra algo).

Realmente es una colección sencilla, y su razón de ser es precisamente ser una colección fuertemente tipada. El siguiente programita muestra un sencillo ejemplo sobre el uso de StringCollection.

using System;
using System.Collections.Specialized;
using System.Linq;

namespace Fermasmas.Wordpress.Com
{
    class Program
    {
        static void Main(string[] args)
        {
            StringCollection strings = new StringCollection();
            strings.Add("Jetzt laden die Vampire zum Tanz");

            string[] strs = new string[] {
                "Steckt den Himmel im Brand",
                "und streut Luzifer Rosen!",
                "die Welt gehört den Lüngern",
                "und den Rücksichtlosen"
            };
            strings.AddRange(strs);
            strings.Insert(1, "Wir wollen alles und Ganz!");

            if (strings.Contains("Jetzt laden die Vampire zum Tanz"))
            {
                int index = strings.IndexOf("Jetzt laden die Vampire zum Tanz");
                Console.WriteLine(strings[index]);
            }

            foreach (string str in strings)
                Console.WriteLine(str);
            Console.WriteLine();

            var query = from string str in strings
                        where str.Contains("und")
                        select str;
            foreach (string str in query)
                Console.WriteLine(str);

            strings.Clear();

            Console.ReadKey(true);
        }
    }
}

StringCollection no cuenta con métodos sofisticados para realizar búsquedas. Sin embargo, nada nos detiene de utilizar algún método en particular, digamos LINQ, como se muestra en las líneas 33 a 37 del ejemplo anterior. A continuación, la salida del programa.

Jetzt laden die Vampire zum Tanz
Jetzt laden die Vampire zum Tanz
Wir wollen alles und Ganz!
Steckt den Himmel im Brand
und streut Luzifer Rosen!
die Welt gehört den Lüngern
und den Rücksichtlosen

Wir wollen alles und Ganz!
und streut Luzifer Rosen!
und den Rücksichtlosen

La segunda clase que me gustaría mostrar es StringDictionary. Así como StringCollection es una implementación similar a ArrayList pero fuertemente tipada, StringDictionary implementa un Hashtable pero fuertemente tipado para emplear cadenas de texto.

Por ende, podemos encontrar métodos similares a los que tenemos en StringCollection: Add y Remove para añadir y quitar pares llave-valor del diccionario, ContainsKey y ContainsValue para saber si una llave o un valor están determinadas, y las propiedades Count, Keys y Values para contar el número de elementos, obtener todas las llaves y todos los valores, respectivamente.

using System;
using System.Collections.Specialized;
using System.Linq;
using System.Collections;

namespace Fermasmas.Wordpress.Com
{
    class Program
    {
        static void Main(string[] args)
        {
            StringDictionary dic = new StringDictionary();
            dic.Add("Chorus 1", "Steckt den Himmel in Brand");
            dic.Add("Chorus 2", "und streut Luzifer Rosen!");
            dic.Add("Chorus 3", "die Welt gehört den Lüngern");
            dic.Add("Chorus 4", "und den Rücksichtlosen");

            Console.WriteLine("Chorus 1: {0}", dic["Chorus 1"]);
            Console.WriteLine();

            foreach (DictionaryEntry entry in dic)
                Console.WriteLine("{0}: {1}", entry.Key, entry.Value);

            foreach (string key in dic.Keys)
                Console.WriteLine(key);

            Console.ReadKey(true);
        }
    }
}

El programa anterior genera esta salida.

Chorus 1: Steckt den Himmel in Brand

chorus 3: die Welt gehört den Lüngern
chorus 2: und streut Luzifer Rosen!
chorus 4: und den Rücksichtlosen
chorus 1: Steckt den Himmel in Brand
chorus 3
chorus 2
chorus 4
chorus 1

Nota que en la salida, cuando pintamos las llaves, nos regresa un valor en minúsculas, cuando nosotros las añadimos en mayúsculas. Esto es importante, dado que internamente StringDictionary no distingue entre mayúsculas y minúsculas para las llaves, una característica a tener en cuenta.

Finalmente, tenemos la colección NameValueCollection. En la práctica, ésta se comporta de forma muy similar a StringDictionary: nos permite agrupar pares llave-valor. Pero esta clase es una colección, no un diccionario, y ahí radica la principal diferencia. Por principio, NameValueCollection nos permite acceder a los elementos vía un índice, además de la llave. Además cuenta con constructores un poco más versátiles que nos permiten especificar el comparador (si es sensible a mayúsculas y minúsculas, por ejemplo).

using System;
using System.Collections.Specialized;
using System.Linq;
using System.Collections;

namespace Fermasmas.Wordpress.Com
{
    class Program
    {
        static void Main(string[] args)
        {
            NameValueCollection col = new NameValueCollection();
            col.Add("Chorus 1", "Steckt den Himmel in Brand");
            col.Add("Chorus 2", "und streut Luzifer Rosen!");
            col.Add("Chorus 3", "die Welt gehört den Lüngern");
            col.Add("Chorus 4", "und den Rücksichtlosen");

            Console.WriteLine("Chorus 1: {0}", col["Chorus 1"]);
            Console.WriteLine();

            foreach (string key in col.AllKeys)
                Console.WriteLine(key);
            Console.WriteLine();

            for (int i = 0; i < col.Count; i++)
                Console.WriteLine("{0}: {1}", col.GetKey(i), col.Get(i));

            Console.ReadKey(true);
        }
    }
}

Su salida:

Chorus 1: Steckt den Himmel in Brand

Chorus 1
Chorus 2
Chorus 3
Chorus 4

Chorus 1: Steckt den Himmel in Brand
Chorus 2: und streut Luzifer Rosen!
Chorus 3: die Welt gehört den Lüngern
Chorus 4: und den Rücksichtlosen

Notarás que la forma de emplear esta clase difiere un poquito de StringDictionary, pero se obtiene prácticamente el mismo resultado.

NameValueCollection se utiliza en muchos lugares del .NET Framework, como en los parámetros de consulta de una página web o bien para leer archivos de configuración. De ahí la importancia que tiene.

¿Cómo saber cuándo usar NameValueCollection y StringDictionary? En esencia, la respuesta se base en si necesitas tener una colección (ICollection) o un diccionario (IDictionary) respectivamente. Aunque se ha de reconocer que NameValueCollection es un poquitín más versátil de StringDictionary.

Ya para terminar, me gustaría que leyeras este pequeño artículo sobre cómo leer la configuración de la aplicación en un archivo web.config, que se encuentra en MSDN, para que veas una aplicación más de NameValueCollection.

Jetzt laden die Vampire zum Tanz, wir wollen alles und ganz!

Categorías:.NET Framework, C#, Tutorial Etiquetas: , ,

Todo lo que siempre quisiste saber sobre colecciones y tenías miedo de preguntar… Banderas y arreglos de bits


Para esta serie de “todo lo que siempre quisiste saber sobre colecciones y tenías miedo de preguntar” me he dado a la tarea de documentar y tratar de explicar las clases que se utilizan para las colecciones, con el propósito de que conozcamos todo lo que el .NET Framework tiene que darnos, en aras de evitar escribir código más estandarizado y robusto. En esta ocasión hablaré de una pequeña clase poco utilizada que bien nos puede salvar de problemas cuando tengamos que trabajar con muchos valores lógicos (booleanos): BitArray.

Esta clase, ubicada en el espacio de nombres System.Collections, nos permite manipular valores booleanos de forma fácil y sencilla. Esto podría no parecer de mucha utilidad, pero hay veces en los que necesitamos crear sistemas que manipulen el estado de un objeto (por ejemplo, una máquina de estados). Cada estado puede ser representado por un valor booleano, o bandera: verdadero o falso, prendido o apagado, etc. Por supuesto, lo primero que viene a la mente es crear una variable de tipo bool para cada bandera. Pero esto puede ser engorroso si tenemos muchas banderas: no solo hace nuestro manejo de banderas algo verboso, sino que hacer operaciones lógicas puede hacernos escribir grandes líneas de código. Es aquí donde BitArray puede sernos de utilidad.

Comencemos por exponer información sobre la clase. Un BitArray, en primer lugar, no permite agregar o añadir elementos: algo contra-intuitivo tratándose de una colección (BitArray implementa ICollection, después de todo). Pero piénsalo de esta forma: un conjunto de estados de un objeto determinado suele estar definido de antemano. En consecuencia, el tamaño del arreglo se determina en el constructor. Veamos algunos constructores.

BitArray bits1 = new BitArray(10);

BitArray bits2 = new BitArray(bits1);

boo[] boolArray = new bool[] { true, true, false, true, false, false, false };
BitArray bits3 = new BitArray(boolArray);

BitArray bits4 = new BitArray(10, true);

byte[] byteArray = new byte[] { 0xff, 0x00 };
BitArray bits5 = new BitArray(byteArray);

La primera llamada nos genera un array con diez bits. La segunda llamada nos genera un array el cual copia el tamaño y los bits contenidos en el primer array. El tercer arreglo se crea con siete bits de la forma 1101000, y la siguiente llamada nos genera un arreglo de diez bits, todos inicializados a 1. Finalmente, la quinta llamada nos genera un array de 16 bits, con los primeros ocho bits establecidos a 1 y los siguientes ocho establecidos a 0.

Como puedes ver, crear un BitArray no es nada complicado. Una vez creado, podemos emplear métodos útiles, de los cuales algunos se implementan de ICollection y otros nos permiten manipular los bits y hacer cálculos sobre ellos. Por ejemplo, Clone nos permite crear un nuevo BitArray copiando los valores del actual, mientras que CopyTo nos permite copiar los valores a un array cualquiera. Get y Set nos permiten leer y escribir un bit en determinada posición, mientras que SetAll establece todos los bits a 1 o 0.

Asimismo, BitArray cuenta con algunos métodos para hacer operaciones lógicas a nivel de bits. And y Or nos permite hacer una comparación lógica conjuntiva y disyuntiva, respectivamente, mientras que Not niega cada uno de los bits. Xor es similar a Or, pero es una disyunción excluyente. Nota que al hacer estas operaciones, se modifica el objeto actual con el resultado. Un ejemplo:

BitArray bits1 = new BitArray(new byte[] { 0xF });
var val = new bool[] { true, true, false, true, true, false, false, true })
BitArray bits2 = new BitArray(val);

bits1.Print();              // 11110000
bits2.Print();              // 11011001
bits1.And(bits2).Print();   // 11010000
bits1.Xor(bits2).Print();   // 00001001
bits1.Or(bits2).Print();    // 11011001
bits1.Not().Print();        // 00100110

El siguiente ejemplo muestra cómo podemos utilizar un BitArray para almacenar el estado de un objeto. Para este caso, creamos una clase llamada Order, que simula una orden de trabajo. Algunos métodos cambian el estado del mismo, por lo que podemos apreciar cómo va evolucionando.

using System;
using System.Collections;
using System.Text;

namespace Fermasmas.Wordpress.Com
{
    class Order
    {
        private BitArray _status;
        private string _number;

        public Order()
        {
            _number = string.Empty;
            _status = new BitArray(8, false);
            _status.Set(0, true);
        }

        public bool IsNew { get { return _status.Get(0); } }
        public bool IsDirty { get { return _status.Get(1); } }
        public bool IsDeleted { get { return _status.Get(2); } }
        public bool IsShipping { get { return _status.Get(3); } }
        public bool IsCanceled { get { return _status.Get(4); } }
        public bool IsComplete { get { return _status.Get(5); } }
        public bool IsError { get { return _status.Get(6); } }
        public bool IsClosed { get { return _status.Get(7); } }

        public override string ToString()
        {
            return new StringBuilder()
                .AppendFormat("New: {0}\n", IsNew)
                .AppendFormat("Dirty: {0}\n", IsDirty)
                .AppendFormat("Deleted: {0}\n", IsDeleted)
                .AppendFormat("Shipping: {0}\n", IsShipping)
                .AppendFormat("Canceled: {0}\n", IsCanceled)
                .AppendFormat("Complete: {0}\n", IsComplete)
                .AppendFormat("Error: {0}\n", IsError)
                .AppendFormat("Closed: {0}\n\n", IsClosed)
                .ToString();
        }

        public string Number
        {
            get { return _number; }
            set
            {
                _number = value;
                _status.Set(1, true);
            }
        }

        public void Delete()
        {
            _status.Set(2, true);
        }

        public void Save()
        {
            _status.Set(1, false);
        }

        public void ShipOrder()
        {
            _status.Set(3, true);
        }

        public void Cancel()
        {
            _status.Set(4, true);
        }

        public void Complete()
        {
            _status.Set(5, true);
        }

        public void ReportError()
        {
            _status.Set(6, true);
        }

        public void Close()
        {
            _status.Set(7, true);
        }

    }

    class Program
    {
        static void Main(string[] args)
        {
            Order order = new Order();
            Console.WriteLine(order);

            order.Number = "99901";
            Console.WriteLine(order);
            order.Save();
            order.ShipOrder();
            Console.WriteLine(order);
            order.ReportError();
            order.Cancel();
            order.Close();
            Console.WriteLine(order);

            Console.ReadKey(true);
        }
    }
}

La salida de este programa es la siguiente.

New: True
Dirty: False
Deleted: False
Shipping: False
Canceled: False
Complete: False
Error: False
Closed: False


New: True
Dirty: True
Deleted: False
Shipping: False
Canceled: False
Complete: False
Error: False
Closed: False


New: True
Dirty: False
Deleted: False
Shipping: True
Canceled: False
Complete: False
Error: False
Closed: False


New: True
Dirty: False
Deleted: False
Shipping: True
Canceled: True
Complete: False
Error: True
Closed: True

Como puedes ver, BitArray es una clase útil de conocer, aunque concedido, no siempre se utiliza. Pero también puedes ver que cuando trabajamos con muchos estados puede ser una forma más eficiente de solucionar el problema.

Eso es todo por el momento. Noi ci vediamo!

ADDENDUM

BitArray tiene una clase muy similar, hermana diría yo: BitVector32, ubicada en el espacio de nombres System.Collections.Specialized. Al igual que BitArray, esta clase se utiliza para guardar valores booleanos (banderas), pero con ciertas diferencias.

En primer lugar, BitVector32 es una estructura, y por tanto, un tipo-valor. Esto quiere decir que si yo asigno una instancia a una variable, dicha instancia será copiada. Y en segundo lugar, BitVector32 tiene un tamaño fijo: 32 bits. Esto la hace más eficiente en ciertos escenarios, dado que BitArray mantiene estados para hacer crecer el tamaño de bits que utiliza.

BitVector32 cuenta, además, con un método (estático) utilísimo cuando tratamos con banderas: CreateMask. Este método genera máscaras de bits en base a una máscara anterior. Por ejemplo, CreateMask() regresa un BitVector con la máscara 00000001. Si llamo otra vez a CreateMask, pasándole como parámetro la primera máscara, me regresa 00000010. Una nueva llamada en forma similar, regresaría 00000100. Y así sucesivamente.

No dejes de darle una revisada a la documentación en MSDN.

Categorías:.NET Framework, C#, Tutorial 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: ,