miércoles, 29 de enero de 2014

Patrones de creación: Prototype

Este post corresponde a la serie Patrones de diseño - Patrones de creación

Prototype

"Especifica los tipos de objetos a crear usando una instancia como prototipo y crea nuevos objetos copiando ese prototipo"

El patrón Prototype es ampliamente conocido, el framework .NET lo implementa mediante la interfaz ICloneable que declara el método Clone(), sin embargo no siempre es comprendido en profundidad.

Prototype es un patrón de creación y se basa en crear objetos mediante la clonación.



El siguiente es el diagrama UML del patrón Prototype:





Estos son los objetos participantes en el patrón Prototype:

  • Prototype. Declara una interfaz para clonarse a sí mismo. En .NET este objeto es la interfaz ICloneable y el método es Clone()
  • ConcretePrototype. Implementa un método para clonarse a sí mismo.
  • Cliente. Crea un objeto llamando al método Clone() del objeto Prototype.

Uso de Prototype para otros fines

La mayoría de nosotros hemos visto y usado el método Clone de ICloneable. Pero en muchas ocasiones no con el fin de instanciar objetos de la aplicación sino más bien para obtener una copia de el estado de un objeto en un momento determinado.

Por ejemplo, si estamos haciendo una funcionalidad que modifica los datos de una factura podríamos crear un clon de la factura justo antes de que el usuario la modifique para conservar una instancia con los valores originales y así poder restaurar los valores originales si el usuario cancela los cambios. O saber exactamente (por comparación con el clon) que propiedades exactamente ha cambiado el usuario.

Prototype como patrón de creación

Una vez que hemos visto que el método Clone de Prototype puede usarse con otros fines, me gustaría centrarme en la finalidad principal o la más interesante desde el punto de vista del diseño de software. Esta funcionalidad como ya hemos visto es la instanciación de nuevos objetos a partir de un prototipo.

En una aplicación existen dos formas habituales de parametrizar la creación de nuevos objetos:
  • Creando subclases (ConcreteCreator) de la clase responsable de crear los objetos (Creator); este es el caso cuando usamos el patrón Factory Method. El inconveniente de esta forma de crear objetos nuevos (Product) es que tenemos que crear nuevas subclases (ConcreteCreator) para cada tipo de objeto nuevo que queremos crear (Product). Esto provoca la proliferación de clases ConcreteCreator aunque sean con diferencias mínimas entre ellas.
  • Los patrones de creación Abstract Factory, Builder y Prototype se basan en la composición de objetos. Un aspecto clave de estos tres patrones es que crean un "objeto factoría" responsable de crear objetos concretos. Abstract Factory usa la clase AbstractFactory para crear objetos concretos de una familia; Builder usa la clase Director para crear objetos complejos paso a paso y siguiendo un protocolo de construcción y Prototype usa su factoría para crear objetos clonándolos a partir de un prototipo. En este caso el objeto factoría y el objeto prototipo es el mismo ya que el prototipo es el responsable de crear el nuevo objeto.
Usar el patrón Prototype permite reducir el número de clases necesarias para crear un objeto y además sólo necesitamos implementar el método Clone

Cuando creamos un objeto mediante la clonación de un prototipo es posible que el estado interno del prototipo no sea correcto para la instancia concreta que estamos creando y tendremos que establecer el estado interno para la instancia que hemos creado.

Dado que no podemos pasar parámetros al método Clone para establecer el estado interno porque cada prototipo tiene sus propios parámetros y perderíamos la interfaz uniforme para clonar; si queremos establecer el estado interno de una instancia clonada tenemos dos opciones principalmente:

  1. Establecer el estado interno asignando propiedades del objeto clonado (si es posible).
  2. Crear un método de inicialización, por ejemplo Initialize, en cada prototipo donde pasamos los parámetros necesarios a cada prototipo para establecer su estado interno.

Normalmente vamos a usar Prototype en los casos en que:
  • Sólo sabemos los objetos concretos que vamos a instanciar en tiempo de ejecución.
  • Si queremos evitar construir una jerarquía de clases factoría paralela a la jerarquia de objetos a construir. (ConcreteCreator - Product)
  • Si la instanciación de un objeto es costosa. Por ejemplo, si hay que leer sus datos de una base de datos o servicio web es mejor instanciar una vez un prototipo, y después crear el resto de instancias clonando ese prototipo.
  • Si el número de objetos a crear es pequeño, o es un objeto con uno o unos cuantos estados internos diferentes. En este caso puede ser más conveniente instalar una serie de prototipos y crear los objetos clonando esos prototipos.

Copia superficial o copia profunda.

Sin duda la parte más difícil a la hora de implementar el patrón Prototype es justamente implementar el método Clone correctamente, especialmente si los objetos que queremos clonar tienen referencias circulares. 

Hoy en día la mayoría de frameworks existentes tienen algún tipo de soporte para implementar la clonación, pero eso no nos evita el problema de decidir si necesitamos una copia superficial o una copia profunda. Es decir: nos bastará sólo con copiar las propiedades por valor y compartir las propiedades por referencia entre el clon y el original; o necesitaremos copiar las propiedades por valor y a su vez clonar también las propiedades por referencia.

La copia superficial es simple y la mayor parte de las veces suficiente. .NET provee el método MemberwiseClone() de la clase Object para crear una copia superficial de un objeto. 

Copiar objetos con estructuras complejas suele requerir hacer copias profundas. Es decir, si nuestro objeto original A hace referencia a otro objeto B también necesitamos clonar el objeto B. De esta forma el objeto original y el clonado serán independientes.

Al implementar el método Clone, la decisión que debemos tomar es si nos vale una copia superficial o necesitamos una copia profunda

Por lo tanto a la hora de clonar un objeto la decisión fundamental será que partes del objeto serán clonadas y que partes serán compartidas entre el original y el clon.

El siguiente código crea una copia superficial de un objeto tipo Person:

public class Address
{
    public string City { get; set; }
    public string Street { get; set; }
    public int Number { get; set; }
    public string Zip { get; set; }
}

public class Person : ICloneable
{
    public string Name { get; set; }
    public int Age { get; set; }
    public Address Address { get; set; }

    public override string ToString()
    {
        return string.Format("{0},{1} años. Vive en {2}, calle {3}", Name, Age, Address.City, Address.Street);
    }

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

class Program
{
    static void Main(string[] args)
    {

        var personPrototype = new Person
        {
            Name = "Miguel A. González",
            Age = 41,
            Address = new Address
            {
                City = "Madrid",
                Street = "C/ Mayor",
                Number = 45,
                Zip = "28075"
            }
        };
        clonedPerson.Age = 40;
        Person clonedPerson = (Person)personPrototype.Clone();

        clonedPerson.Address.City = "Barcelona";

        System.Console.WriteLine(personPrototype);
        System.Console.WriteLine(clonedPerson);

        System.Console.ReadLine();
    }
}

El resultado lo podemos ver en la siguiente imagen.


Como podemos ver, tanto personPrototype como clonedPerson tienen la misma dirección. Esto es debido a que en el método Clone() de Person hacemos una copia superficial.

En una copia superficial se copian las propiedades por valor y se comparten las propiedades por referencia. Por eso al cambiar la propiedad Address.City del clon cambia igualmente la ciudad del original (Address se comparte entre original y clon).

Ahora haremos lo mismo pero creando una copia profunda de Person:
public class Address : ICloneable
{
    public string City { get; set; }
    public string Street { get; set; }
    public int Number { get; set; }
    public string Zip { get; set; }

    public object Clone()
    {
        return new Address
        {
            City = this.City,
            Street = this.Street,
            Number = this.Number,
            Zip = this.Zip
        };
    }
}

public class Person : ICloneable
{
    public string Name { get; set; }
    public int Age { get; set; }
    public Address Address { get; set; }

    public override string ToString()
    {
        return string.Format("{0},{1} años. Vive en {2}, calle {3}", Name, Age, Address.City, Address.Street);
    }

    public object Clone()
    {
        return new Person
        {
            Name = this.Name,
            Age = this.Age,
            Address = (Address)this.Address.Clone()
        };
    }
}

class Program
{
    static void Main(string[] args)
    {
        var personPrototype = new Person
        {
            Name = "Miguel A. González",
            Age = 41,
            Address = new Address
            {
                City = "Madrid",
                Street = "C/ Mayor",
                Number = 45,
                Zip = "28075"
            }
        };

        Person clonedPerson = (Person)personPrototype.Clone();

        clonedPerson.Age = 40;
        clonedPerson.Address.City = "Barcelona";

        System.Console.WriteLine(personPrototype);
        System.Console.WriteLine(clonedPerson);

        System.Console.ReadLine();
    }
}

Este código produce la siguiente salida en la consola:

Como vemos ahora el objeto original y le objeto clonado son instancias completamente distintas, no comparten el objeto Address.

Una práctica común al clonar en .NET

En .NET podemos crear una copia profunda de un objeto serializando el original en memoria y de-serializándolo posteriormente sobre el clon. La única restricción que este método implica es que el objeto original como los objetos a los que se hace referencia deben ser serializables. El siguiente código muestra un ejemplo en el que es posible aplicar esta técnica y otro en el que no lo es.

[Serializable]
public class Address
{
    public string City { get; set; }
    public string Street { get; set; }
    public int Number { get; set; }
    public string Zip { get; set; }
}

[Serializable]
public class Person
{
    public string Name { get; set; }
    public int Age { get; set; }
    public Address Address { get; set; }

    public override string ToString()
    {
        return string.Format("{0},{1} años. Vive en {2}, calle {3}", Name, Age, Address.City, Address.Street);
    }
}

class Program
{
    static void Main(string[] args)
    {
        var personPrototype = new Person
        {
            Name = "Miguel A. González",
            Age = 41,
            Address = new Address
            {
                City = "Madrid",
                Street = "C/ Mayor",
                Number = 45,
                Zip = "28075"
            }
        };

        Person clonedPerson = null;

        using (var memoryStream = new System.IO.MemoryStream())
        {
            var binaryFormatter = new System.Runtime.Serialization.Formatters.Binary.BinaryFormatter();
            binaryFormatter.Serialize(memoryStream, personPrototype);
            memoryStream.Position = 0;
            clonedPerson = (Person)binaryFormatter.Deserialize(memoryStream);
        }

        clonedPerson.Age = 40;
        clonedPerson.Address.City = "Barcelona";

        System.Console.WriteLine(personPrototype);
        System.Console.WriteLine(clonedPerson);

        System.Console.ReadLine();
    }
}

Si ejecutamos este código veremos que el objeto original (personPrototype) y el objeto clonado (clonedPerson) evolucionan de forma independiente, cada uno tiene sus propiedades no compartidas, sus direcciones son distintas. Para esta técnica hay que reseñar el atributo [Serializable] que hemos aplicado tanto a la clase Person como a la clase Address para que puedan serializarse.

PrototypeManager

Cuando decidimos usar Prototype como método de creación de instancias en nuestra aplicación, o en una parte de ella, y el número de prototipos no es fijo sino que pueden variar de forma dinámica (se pueden crear y destruir prototipos en tiempo de ejecución); la forma habitual de gestionar los prototipos es mediante un PropotypeManager.

La clase PrototypeManager tiene la responsabilidad de mantener un registro de los prototipos activos en la aplicación. PrototypeManager permite a los clientes registrar y recuperar prototipos mediante una clave, eliminar un prototipo del registro, navegar por los prototipos, obtener un inventario de los prototipos activos y cualquier otra funcionalidad adicional que nuestra aplicación requiera.

Veamos cómo podría ser una implementación básica de un PrototypeManager:


public class Address : ICloneable
{
    public string City { get; set; }
    public string Street { get; set; }
    public int Number { get; set; }
    public string Zip { get; set; }

    public object Clone()
    {
        return new Address
        {
            City = this.City,
            Street = this.Street,
            Number = this.Number,
            Zip = this.Zip
        };
    }
}

public class Person : ICloneable
{
    public string Name { get; set; }
    public int Age { get; set; }
    public Address Address { get; set; }

    public override string ToString()
    {
        return string.Format("{0},{1} años. Vive en {2}, calle {3}", Name, Age, Address.City, Address.Street);
    }

    public object Clone()
    {
        return new Person
        {
            Name = this.Name,
            Age = this.Age,
            Address = (Address)this.Address.Clone()
        };
    }
}

public class PrototypeManager
{
    private Dictionary<string, ICloneable> registry;

    public PrototypeManager()
    {
        registry = new Dictionary<string, ICloneable>();
    }

    public void RegisterPrototype(string key, ICloneable prototype)
    {
        registry.Add(key, prototype);
    }

    public void UnregisterPrototype(string key)
    {
        if (registry.ContainsKey(key))
            registry.Remove(key);
    }

    public ICloneable GetPrototype(string key)
    {
        if (registry.ContainsKey(key))
            return registry[key];

        return null;
    }
}

class Program
{
    static void Main(string[] args)
    {
        var prototypeManaer = new PrototypeManager();
        RegisterPrototypes(prototypeManaer);

        // Obtenemos los prototipos, los clonamos y asignamos las propieades necesarias.
        var prototype1 = prototypeManaer.GetPrototype("madrid-middle-aged");
        var person1 = (Person)prototype1.Clone();
        person1.Name = "Miguel Angel";
        person1.Address.Street = "C/ Mayor";
        person1.Address.Number = 45;

        var prototype2 = prototypeManaer.GetPrototype("barcelona-Teenager");
        var person2 = (Person)prototype2.Clone();
        person2.Name = "Alba";
        person2.Address.Street = "C/ Rambla";
        person2.Address.Number = 45;


        Console.WriteLine(person1);
        Console.WriteLine(person2);

        Console.ReadLine();
    }

    private static void RegisterPrototypes(PrototypeManager prototypeManaer)
    {
        var madridMiddleAgePrototype = new Person
        {
            Age = 40,
            Address = new Address
            {
                City = "Madrid",
                Zip = "28075"
            }
        };

        var barcelonaTeenAgerPrototype = new Person
        {
            Age = 16,
            Address = new Address
            {
                City = "Barcelona",
                Zip = "08075"
            }
        };

        prototypeManaer.RegisterPrototype("madrid-middle-aged", madridMiddleAgePrototype);
        prototypeManaer.RegisterPrototype("barcelona-Teenager", barcelonaTeenAgerPrototype);
    }
}

Hasta aquí el patrón Prototype. Como ya he mencionado es un patrón conocido pero a veces más por el método Clone que permite hacer una copia de un objeto que como patrón de creación. Espero que este post haya ayudado a comprenderlo en mayor profundidad.

En el próximo post veremos el patrón Singleton.

No hay comentarios:

Publicar un comentario en la entrada