Patrones Estructurales (V): Patrón Flyweight


Objetivo:

«Compartir una parte común del estado de un objeto para hacer más eficiente la gestión de un número elevado de objetos de grano más fino.»

Design Patterns: Elements of Reusable Object-Oriented Software

El patrón Flyweight u «objeto ligero», a diferencia de algunos de los patrones vistos hasta el momento, no goza de un uso masivo entre la comunidad de ingenieros de software. El motivo no radica en su falta de utilidad o de interés, sino que se centra principalmente en el concepto de rendimiento. A grandes rasgos, se basa en dividir un objeto en dos partes: una parte «común» a un conjunto grande de los objetos de la clase (parte intrínseca), y una parte «privada» que será accesible y modificable únicamente por un objeto en concreto (parte extrínseca).

Un ejemplo simple de Flyweight podría ser el gestor de ventanas del sistema operativo. Un tema de ventanas poseerá atributos como color de fondo, fuente tipográfica, grosor del borde de la ventana, estilo de los botones… Muchos de estos atributos serán comunes a todas las ventanas, por lo que podríamos almacenar toda esta información en un elemento compartido y hacer una llamada a un método para que, haciendo uso de estos atributos comunes y de los parámetros recibidos por el método, se realice una operación que no requiera una instancia exclusiva para ello.

Por ejemplo, realizando una llamada a un método como RenderizarVentana(int x, int y, int ancho, int alto), nuestro objeto Flyweight utilizaría los valores almacenados comunes a todas las ventanas para dibujar una ventana en la posición (x, y) con unas dimensiones (ancho, alto). La instancia de la ventana será única, limitándose a devolver un objeto con las características indicadas. La alternativa simple, como podremos imaginar, sería instanciar un objeto por cada ventana presente en el sistema que almacenara todos estos datos de forma individual más, opcionalmente, valores específicos para la posición y las dimensiones de la ventana. Como siempre ocurre con los patrones de diseño, simplificar una parte del software implicará aumentar la complejidad de otra. En este caso, un diseño más simple se corresponderá con un aumento de consumo de memoria. El límite, nuevamente, se encontrará en el punto de equilibrio que el diseñador estime conveniente.

 

Flyweight y Factory Pattern

El patrón Flyweight está íntimamente ligado a otro de los patrones que ya hemos visto: el patrón factoría o Factory Pattern. El motivo no es otro que permitir que sea el objeto que implementa este patrón el que gestione la separación entre la parte «común» (denominada intrínseca) y la parte «privada» (denominada extrínseca), centralizando el proceso y evitando así que perdamos referencias por el camino si realizamos el proceso de una forma un poco más artesanal.

Por lo tanto, dentro de un patrón Flyweight, distinguiremos entre estos dos tipos de datos:

  • Intrínsecos: son los datos compartidos por todos los objetos de un subtipo determinado. Por norma general, son datos que no cambiarán a lo largo del tiempo, y si cambian, alterarán el estado de todos los objetos que hagan uso de ellos.
  • Extrínsecos: se calculan «al vuelo» fuera del objeto Flyweight. Este cálculo suele realizarse a partir de los datos intrínsecos y de los parámetros recibidos por los métodos del objeto Flyweight. La idea detrás de los datos extrínsecos radica en que, o bien sean calculados a partir de los datos intrínsecos o bien ocupen una cantidad de memoria mínima en comparación a éstos.

Vehículos ligeros

Acudamos a nuestra flota de vehículos para ilustrar este patrón. Imaginemos que la DGT nos encarga una aplicación que se encargue de gestionar toda la flota de vehículos del país, almacenando datos como marca, modelo, color, matrícula, fecha de matriculación y NIF del titular. En un principio, antes de hacer uso de este patrón, podríamos contar con una clase Vehiculo que tuviese un aspecto similar al siguiente:


    public class Vehiculo
    {
        public string Marca { get; set; }
        public string Modelo { get; set; }
        public string Color { get; set; }
        public string Matricula { get; set; }
        public DateTime FechaMatriculacion { get; set; }
        public string NifTitular { get; set; }


        public Vehiculo(string marca, string modelo, string color, string matricula,
            DateTime fechaMatriculacion, string nifTitular)
        {
            this.Marca = marca;
            this.Modelo = modelo;
            this.Color = color;
            this.Matricula = matricula;
            this.FechaMatriculacion = fechaMatriculacion;
            this.NifTitular = nifTitular;
        }

        public void MostrarCaracteristicas()
        {
            Console.WriteLine(Marca + " " + Modelo + " de color " + Color + 
                " con matricula " + Matricula + " (" + FechaMatriculacion + 
                ") registrado por " + NifTitular);
        }
    }

Esto se representaría en memoria de la siguiente forma:

Intuitivo, ¿verdad? Sin embargo, un buen día nuestro analista recibe una petición de cambio que especifica que, debido a recortes de presupuesto, la máquina que se encarga de gestionar estos vehículos tendrá una cantidad de memoria muy limitada, por lo que se nos pide que optimicemos su consumo todo lo que podamos. El patrón Flyweight entra en escena.

Separando parte intrínseca y parte extrínseca

Lo primero que debemos preguntarnos a la hora de aplicar este patrón es: ¿qué parte de los datos es factorizable? Es decir, ¿se puede sacar un «factor común» a un gran número de casos? ¿Es posible aglutinar en una parte del objeto un conjunto de información redundante que no es probable que vaya a cambiar? Esa información ¿ocupa mucho espacio en memoria?

Si somos capaces de detectar un grupo de elementos que se ajusten a la respuesta a una o varias de estas preguntas, tenemos un candidato para aplicar el patrón. En nuestro caso, supongamos que los campos «Marca», «Modelo» y «Color» se repiten con gran frecuencia (y ya puestos a imaginar, imaginemos que su coste computacional y de consumo de memoria fuera elevado). Sería perfectamente posible extraer estos elementos en un modelo estable cuyos miembros serán referenciados en lugar de instanciados con cada nuevo objeto, ya que como hemos dicho, la información no será susceptible de cambiar con el tiempo (un Seat Panda siempre será un Seat Panda, aunque el atributo «Color» sí que podría darnos problemas en un futuro si lo incluimos como en este caso).

Por lo tanto, generaremos una clase abstracta «Modelo» que permitirá especializaciones e incluirá los atributos, propiedades y métodos comunes. Por ejemplo:


    public abstract class ModeloVehiculo
    {
        public string Marca { get; set; }
        public string Modelo { get; set; }
        public string Color { get; set; }

        public ModeloVehiculo(string marca, string modelo, string color)
        {
            this.Marca = marca;
            this.Modelo = modelo;
            this.Color = color;
        }

        public virtual void MostrarCaracteristicas(String datosExtra)
        {
            Console.WriteLine(Marca + " " + Modelo + " de color " + Color + " " + "\n" + datosExtra + "\n");
        }
    } 

¿Por qué una clase abstracta en lugar de utilizar una clase normal y corriente? Recordemos que nuestros contratos deben depender de abstracciones, no de concreciones. Crearemos una clase que herede de nuestra abstracción para permitir posibles extensiones de nuestra funcionalidad.


    public class ModeloBerlina : ModeloVehiculo
    {
        internal ModeloBerlina(string marca, string modelo, string color) 
            : base(marca, modelo, color)
        {
        }

        public override void MostrarCaracteristicas(string datosExtra)
        {
            base.MostrarCaracteristicas(datosExtra);
        }
    }

Incluso podemos crear una nueva clase que modifique ligeramente el funcionamiento de la clase anterior:


    public class ModeloDeportivo : ModeloVehiculo
    {
        internal ModeloDeportivo(string marca, string modelo, string color) 
            : base(marca, modelo, color)
        {
        }

        public override void MostrarCaracteristicas(string datosExtra)
        {
            base.MostrarCaracteristicas(datosExtra + " edicion Sport");
        }
    }

Con esto habríamos encapsulado la parte intrínseca (común) de nuestro vehículo. Ahora nuestro mapa de memoria habrá evolucionado hacia el siguiente esquema:

Pero no sólo de atributos vive el hombre. Hará falta algún tipo de mecanismo que nos permita instanciar esta parte común manteniendo algún tipo de control sobre qué partes han sido ya instanciadas (y por tanto, se pueden reutilizar) y que partes aún no lo han sido. Y para ello haremos uso, tal y como avanzamos al comienzo del artículo, de un patrón factoría.

Factoría y Flyweight caminan de la mano

Si echamos la vista atrás, nos daremos cuenta de que los constructores de las clases concretas ModeloBerlina y ModeloDeportivo no son públicos, sino internal. ¿Qué significa esto y por qué realizamos esta operación?

El modificador internal limita la visibilidad a aquellas clases que pertenezcan al mismo espacio de nombres que el elemento decorado con este modificador. De este modo, sólo una clase perteneciente al mismo paquete que ModeloBerlina o ModeloDeportivo podrá realizar un new. ¿Qué conseguimos con esto? Que el proceso de instanciado de estas clases esté controlado. Si el usuario no puede crear instancias de estas clases podremos controlar mejor qué elementos han sido ya instanciados y cuáles quedan por instanciar. Y todo esto lo centralizaremos en una clase que hará las veces de factoría.

Nuestra factoría contará con un «pool» que almacenará aquellos objetos que ya han sido instanciados previamente. Este pool no será otra cosa que una colección de pares clave-valor, en los que la clave será un elemento que identifique de forma única a la parte intrínseca (el NIF de una persona, una combinación de marca, modelo y color…) y el valor será una referencia a una instancia de la parte común (que siempre será la misma).

De este modo, la factoría presentará un método en el que se le solicitará una instancia de la parte común del vehículo (ModeloVehiculo). Este método, en lugar de hacer un simple new(), comprobará si este objeto ya había sido instanciado con anterioridad o no. En caso afirmativo, se limitará a devolver la referencia al objeto. En caso de que el objeto no exista, se instanciará (ahora sí, mediante new()), se insertará en el pool para poder referenciarse en posteriores solicitudes y, por último, se devolverá a la clase cliente.


    public class VehiculoFactory
    {
        // El pool se encargará de almacenar las instancias de los objetos reutilizables
        private static Dictionary<string, ModeloVehiculo> pool = new Dictionary<string, ModeloVehiculo>();

        public static ModeloVehiculo GetCar(string marca, string modelo, string color)
        {
            ModeloVehiculo v = null;

            // Si el modelo ya ha sido creado anteriormente, se recupera del pool
            if (pool.ContainsKey(marca + " " + modelo + " " + color))
            {
                v = pool[marca + " " + modelo + " " + color];
                Console.WriteLine("\t* Recuperando del pool el vehiculo " + marca + " " + modelo + " " + color);
            }

            // En caso de que no exista, se instancia un nuevo objeto y se añade al pool.
            // Las próximas ocasiones en las que el objeto sea utilizado, se devolverá una referencia
            // al objeto existente, evitando ocupar más memoria en crear una nueva instancia
            else
            {
                // Dependiendo de algún parámetro (por ejemplo, si el modelo tiene el sufijo 'sport'), se
                // instanciará una clase u otra.
                if (modelo.EndsWith("sport"))
                    v = new ModeloDeportivo(marca, modelo, color);
                else
                    v = new ModeloBerlina(marca, modelo, color);

                // Se añade el objeto al pool: las sucesivas llamadas usarán este objeto en lugar de
                // instanciar uno nuevo
                pool.Add(marca + " " + modelo + " " + color, v);
                Console.WriteLine("\t* Insertando en el pool el vehiculo " + marca + " " + modelo + " " + color);
            }

            return v;
        }
    }

Hemos añadido un par de «Console.WriteLine» para ayudarnos a trazar cuándo un objeto está siendo instanciado y cuándo se está recuperando del propio pool.

Uniendo datos intrínsecos y datos extrínsecos

Como último paso, crearemos la clase cliente, que será el vehículo que modelamos al principio del artículo. La clase cliente no tiene por qué saber que una parte de nuestro objeto es común a otros objetos: sólo quiere utilizar la información de la forma más transparente posible. Por ello, crearemos una referencia a la parte implícita (ModeloVehiculo) y modelaremos de la forma habitual los datos explícitos que variarán en cada instancia (matrícula, fecha de matriculación y NIF del titular).

El constructor de nuestra clase tomará seis parámetros: los tres primeros serán pasados a la factoría para que nos proporcione un objeto implícito (sólo se instanciará una única vez). Los tres parámetros siguientes serán parte exclusiva de nuestro objeto (datos explícitos).

Si queremos hacer la clase aún más transparente al cliente, bastará con encapsular los métodos y propiedades del objeto implícito, exponiendo mediante métodos o propiedades de sólo lectura su funcionalidad.


    public class Vehiculo
    {
        // Los datos implícitos estarán encapsulados dentro de la clase ModeloVehiculo
        private ModeloVehiculo datosImplicitos;

        // Datos explícitos
        public string Matricula { get; set; }
        public DateTime FechaMatriculacion { get; set; }
        public string NifTitular { get; set; }

        // Propiedades de acceso a los elementos implícitos.
        // Recordemos que estos datos no deberían variar con el tiempo (son comunes a todas las
        // instancias) y, si lo hicieran, afectarían a todas las instancias.
        public string Marca { get { return datosImplicitos.Marca; } }
        public string Modelo { get { return datosImplicitos.Modelo; } }
        public string Color { get { return datosImplicitos.Color; } }

        // Constructor del vehículo
        // Hace uso de la factoría para obtener (o generar, en caso de que no exista) la parte implícita de
        // los datos del vehículo (marca, modelo y color)
        public Vehiculo(string marca, string modelo, string color,                  // Datos implícitos
            string matricula, DateTime fechaMatriculacion, string nifTitular)       // Datos explícitos
        {
            // Instanciamos o referenciamos los datos implícitos a través de la factoría
            this.datosImplicitos = VehiculoFactory.GetCar(marca, modelo, color);

            // Asignamos los datos propios, exclusivos de este objeto
            this.Matricula = matricula;
            this.FechaMatriculacion = fechaMatriculacion;
            this.NifTitular = nifTitular;
        }

        // Método que accede tanto a datos implícitos como a datos explícitos
        public void MostrarInformacionVehiculo()
        {
            datosImplicitos.MostrarCaracteristicas(" con matricula " + Matricula + 
                " (" + FechaMatriculacion.ToShortDateString() + 
                ") registrado por " + NifTitular);
        }
    }

Como ejemplo de este patrón, crearemos cinco vehículos: los dos primeros compartirán marca y modelo, pero no color, por lo que crearán partes implícitas distintas. El último, sin embargo, compartirá los tres atributos. Esto hará que, al instanciar un nuevo vehículo, la instancia de la parte implícita sea reutilizada, evitando que dichos datos se dupliquen en memoria.


            Vehiculo v1 = new Vehiculo("Seat", "Ibiza sport", "Amarillo", "1234-CCA", DateTime.Now, "71000011A");
            Vehiculo v2 = new Vehiculo("Seat", "Ibiza sport", "Rojo", "1235-CCA", DateTime.Now, "71000012A");
            Vehiculo v3 = new Vehiculo("Peugeot", "406", "Verde", "1236-CCA", DateTime.Now, "71000013A");
            Vehiculo v4 = new Vehiculo("Renault", "Clio sport", "Amarillo", "1237-CCA", DateTime.Now, "71000014A");
            Vehiculo v5 = new Vehiculo("Seat", "Ibiza sport", "Amarillo", "1238-CCA", DateTime.Now, "71000015A");

            v1.MostrarInformacionVehiculo();
            v2.MostrarInformacionVehiculo();
            v3.MostrarInformacionVehiculo();
            v4.MostrarInformacionVehiculo();
            v5.MostrarInformacionVehiculo();

            Console.ReadLine();

Como recordatorio, es necesario tener en cuenta que el recolector de basura de .NET liberará la memoria usada por un objeto cuando no exista ninguna referencia apuntando hacia ella, es decir, cuando el último de los objetos que referencie el objeto haya sido destruido. Y dado que nuestra factoría mantendrá siempre una copia en el pool, esta memoria permanecerá ocupada de manera permanente hasta que finalice el programa.

Si queremos evitar este comportamiento, podremos codificar nuestra factoría como un objeto normal en lugar de hacerlo como una clase estática. Todo dependerá, de nuevo, del uso que se le pretenda dar y del coste que los recursos tengan en nuestro programa.

¿Cuándo utilizar este patrón? Ejemplos reales

El escenario más común para utilizar este patrón suele ser en la programación gráfica. Por norma general, cuando se trata con sprites, éstos se cargarán una única vez en memoria, haciendo que los objetos los referencien y almacenen en memoria únicamente una posición relativa.

Otros posibles usos de este patrón serían el cacheo de estilos. Por ejemplo, al exportar datos a Excel, sería posible almacenar como objetos implícitos los estilos de las celdas, referenciándolos únicamente cuando se vayan a utilizar y evitando duplicar la información asociada a ellos.

Si buscamos un ejemplo de la API de Java o .NET, puede que nos sorprenda encontrarnos con que la clase Integer de Java hace uso de esta técnica para encapsular el tipo primitivo int, usando elementos flyweight para representar los índices entre -128 y 127 (los más utilizados). En caso de no hacer uso de esta técnica, un elemento Integer ocuparía, como podríamos imaginar, una cantidad enorme de memoria.

Fuentes:

Pro JavaScript Design Patterns (Dustin Díaz et al.)

Flyweight Design Pattern (DevLake, Codeproject)

Game Programming Patterns. Design Patterns Revisited (Bob Nystrom)

Stackexchange.com forums (Varios autores)

7 comentarios

  1. Ya echaba de menos el patrón semanal.

    Me ha gustado mucho la idea de este patrón aunque hasta ahora nunca me he enfrentado con limitaciones de memoria en ninguna de las aplicaciones – servidores. Me lo apunto.

    Gracias Daniel.

    Esperando por más.

  2. Hola Daniel, muy buenas explicaciones, excelentes diría yo

    Una pregunta sobre patrones, ¿tenés pensado hablar sobre singleton?

    Gracias y nuevamente genial tu web.

  3. Extremadamente útiles los artículos, los leo con mi abuela(desde el primero) XD
    Muchas gracias Daniel !!!!!!!!!!!!!

Deja un comentario