Patrones de creación (III): Patrón Prototype


Objetivo:

“Especificar el tipo de objetos que se crearán utilizando una instancia prototipada y crear nuevos objetos realizando copias de ese prototipo.”

Design Patterns: Elements of Reusable Object-Oriented Software

El concepto de este patrón es simple: en lugar de crear un objeto, se clona, es decir, se realiza una copia exacta de otro objeto dado, denominado prototipo.

Entran en juego tres elementos:

  • Cliente: clase que solicita al prototipo que se clone.
  • IPrototipo: interfaz o clase abstracta que define la operación de clonado.
  • PrototipoConcreto: implementa IPrototipo y su método Clone() para proceder al clonado del objeto.

El proceso de clonado comienza instanciando una clase de forma habitual. Una vez que disponemos de una instancia funcional, el resto de instancias se generarán creando copias de la primera.

La forma de aplicar este patrón es simple:

  • Se define una interfaz que expondrá el método utilizado para realizar el clonado del objeto.
  • Las clases que realicen el clonado utilizarán este método para esta operación.

Implementación del patrón

En .NET este proceso es sencillo, ya que nos ofrece la interfaz ICloneable que expone el método Clone(), método en el que habrá que codificar el proceso de copia.


    public class Vehiculo : ICloneable
    {

        public string Marca { get; set; }
        public string Modelo { get; set; }
        public string Color { get; set; }
        public Rueda TipoRueda { get; set; }
        public Carroceria TipoCarroceria { get; set; }


        #region ICloneable Members

        public object Clone()
        {
            throw new NotImplementedException();
        }

        #endregion
    }

Además de ofrecer el método Clone(), .NET también ofrece un método, MemberwiseClone(), que automáticamente realiza una copia del objeto por nosotros, evitándonos el proceso de copiar elemento por elemento de forma manual.

Realicemos un ejemplo de uso de este patrón. Comenzaremos por definir completamente las clases involucradas: Vehiculo, Rueda y Carroceria:

Vehiculo:


    public class Vehiculo : ICloneable
    {
        public string Marca { get; set; }
        public string Modelo { get; set; }
        public string Color { get; set; }
        public Rueda TipoRueda { get; set; }
        public Carroceria TipoCarroceria { get; set; }

        public string VehiculoInfo()
        {
            StringBuilder sb = new StringBuilder();
            sb.Append("Marca: ").Append(Marca).Append(Environment.NewLine);
            sb.Append("Modelo: ").Append(Modelo).Append(Environment.NewLine);
            sb.Append("Color: ").Append(Color).Append(Environment.NewLine);
            sb.Append("Ruedas: ").Append(TipoRueda.Llanta).Append(" ");
            sb.Append(TipoRueda.Diametro).Append(" ").Append(TipoRueda.Neumatico).Append(Environment.NewLine);
            sb.Append("Carroceria: ").Append(TipoCarroceria.HabitaculoReforzado).Append(" ");
            sb.Append(TipoCarroceria.TipoCarroceria).Append(" ").Append(TipoCarroceria.Material).Append(Environment.NewLine);

            return sb.ToString();
        }

        #region ICloneable Members

        public object Clone()
        {
            return this.MemberwiseClone();
        }

        #endregion
    }

Rueda:


    public class Rueda
    {
        public int Diametro { get; set; }
        public string Llanta { get; set; }
        public string Neumatico { get; set; }
    }

Carroceria:


    public class Carroceria
    {
        public bool HabitaculoReforzado { get; set; }
        public string Material { get; set; }
        public string TipoCarroceria { get; set; }
    }

Crearemos a continuación un nuevo objeto de tipo Vehiculo, lo rellenaremos y después lo clonaremos, mostrando ambos contenidos mediante el método VehiculoInfo():


            Vehiculo v = new Vehiculo();

            v.Marca = "Peugeot";
            v.Modelo = "306";
            v.Color = "Negro";

            v.TipoCarroceria = new Carroceria();
            v.TipoCarroceria.Material = "Acero";
            v.TipoCarroceria.HabitaculoReforzado = true;
            v.TipoCarroceria.TipoCarroceria = "Monovolumen";

            v.TipoRueda = new Rueda();
            v.TipoRueda.Neumatico = "Bridgestone";
            v.TipoRueda.Llanta = "Aluminio";
            v.TipoRueda.Diametro = 17;

            Vehiculo v2 = v.Clone() as Vehiculo;

            Console.WriteLine(v.VehiculoInfo());
            Console.WriteLine("--------------------------------------");
            Console.WriteLine(v2.VehiculoInfo());

            Console.ReadLine();

Ejecutar este código dará pie al siguiente resultado:

Como observamos, ambos objetos contienen exactamente los mismos datos. Probemos a modificar ahora el valor de uno de sus elementos justo después de realizar la clonación, por ejemplo, el color.


            v2.Color = "Rojo";

Si ejecutamos nuevamente nuestro programa, vemos que el segundo coche tiene un color distinto: se trata de una instancia diferente, por lo que una vez que se ha producido la clonación, estos mantienen un estado independiente.

Realicemos la misma operación modificando una de las ruedas.


            v2.TipoRueda.Diametro = 15;
            v2.TipoRueda.Neumatico = "Michelin";
            v2.TipoRueda.Llanta = "Aleación";

Si ejecutamos nuestra aplicación veremos que, en esta ocasión, pese a que hemos modificado el objeto del segundo elemento, el objeto del primero también ha sido modificado:

¿Por qué ocurre esto? Si tenemos un poco de experiencia en programación orientada a objetos probablemente ya sepamos de qué se trata. En caso contrario, no está de más explicarlo.

Clonación superficial y clonación profunda

Cuando invocamos el método MemberwiseClone() observamos que en su descripción indicaba que se realizaba una clonación shallow o superficial. Esto significa que el clonado se realiza a nivel de bits, por lo que los objetos contenidos dentro del objeto a clonar no se clonarán también, sino que se clonará únicamente la referencia del objeto. Por lo tanto, ambos objetos clonados apuntarán al mismo objeto. Esto es lo que se conoce como clonación superficial. El proceso por el cual se clonan los objetos incluidos en el objeto a clonar en lugar de copiar sus referencias se denomina clonación profunda.

Por lo tanto si deseamos realizar una clonación profunda, deberemos realizarla de forma manual. Por ejemplo, haciendo que las clases dependientes del objeto a clonar puedan a su vez ser clonados:


    public class Rueda : ICloneable
    {
        public int Diametro { get; set; }
        public string Llanta { get; set; }
        public string Neumatico { get; set; }

        #region ICloneable Members

        public object Clone()
        {
            return this.MemberwiseClone();
        }

        #endregion
    }

    public class Carroceria : ICloneable
    {
        public bool HabitaculoReforzado { get; set; }
        public string Material { get; set; }
        public string TipoCarroceria { get; set; }

        #region ICloneable Members

        public object Clone()
        {
            return this.MemberwiseClone();
        }

        #endregion
    }

Una vez hecho esto, añadiremos el siguiente código a nuestra clase principal para realizar el siguiente proceso:

  • Recorrer las propiedades del objeto que implementan la interfaz ICloneable
  • Extraer el nombre de la propiedad (por ejemplo Rueda)
  • Invocar su método Clone() mediante Reflection y guardar la referencia al nuevo objeto.
  • Hacer que la referencia del objeto clonado (Vehiculo) apunte al nuevo objeto que hemos clonado mediante reflection (Rueda)

Esto podría lograrse del siguiente modo


        public object Clone()
        {
            // Obtenermos una copia superficial del objeto actual
            object copia = this.MemberwiseClone();

            // Recorremos las propiedades del objeto buscando elementos clonables.
            // En caso de encontrar un objeto clonable, realizamos una copia de dicho elemento
            var propiedadesClonables = this.GetType().GetProperties().Where(p => p.PropertyType.GetInterfaces().Contains(typeof(ICloneable)));
            foreach (var propiedad in propiedadesClonables)
            {
                // Obtenemos el nombre de la propiedad (p.e. "TipoRueda")
                var nombrePropiedad = propiedad.Name;
                
                // Localizamos el método Clone() de la propiedad (TipoRueda.Clone()) y lo
                // invocamos mediante reflection, almacenando el objeto resultante en una variable
                MethodInfo metodoClone = propiedad.PropertyType.GetMethod("Clone");
                var objetoCopia = metodoClone.Invoke(propiedad.GetValue(copia), null);

                // Obtenemos una referencia a la propiedad del objeto clonado (Vehiculo2.TipoRueda)
                PropertyInfo referenciaCopia = this.GetType().GetProperty(nombrePropiedad, BindingFlags.Public | BindingFlags.Instance);

                // Asignamos el valor del objeto clonado a la referencia (Vehiculo2.TipoRueda = Rueda2)
                referenciaCopia.SetValue(copia, objetoCopia, null);
            }

            return copia;
        }

El resultado sería el siguiente:

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

Este patrón está indicado en los casos en los que el coste de generar una nueva instancia sean altos en comparación al coste de realizar una copia de una instancia ya existente. Por ejemplo, imaginemos que tenemos una clase que se encarga de almacenar datos de una sesión web. Al instanciar esta clase, el objeto se comunica con un servidor externo para realizar un proceso de autenticación mediante unas credenciales. Este proceso es costoso, por lo que si a lo largo de la ejecución del programa fuese necesario instanciar un nuevo objeto con los mismos datos, el proceso de instanciación requerirá consumir una gran cantidad de recursos. Sería más sencillo realizar una copia exacta del objeto anterior, evitando de este modo acceder a un servidor externo para rellenar el objeto actual.

Podemos sustituir el proceso de conectar a otro servidor por cualquier otro proceso de alto coste computacional, como operaciones de acceso de base de datos o procesamiento de algoritmos complejos. Siempre que necesitemos instanciar un objeto cuyos datos han sido obtenidos previamente podemos recurrir a este patrón para evitar el proceso de instanciado realizando una copia de un objeto existente.

Otro posible escenario en el que este patrón es útil puede ser cuando sea necesario “salvar” el estado de un objeto en un determinado momento realizando una copia del mismo.

Un escenario real en el que se suele utilizar este patrón suele ser el de la clonación de figuras en programación 2D/3D, así como el de paletas de colores.

Fuentes:

Anuncios

One comment

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