10. Prototype (Prototipo) - Patrón Creacional

El patrón Prototype propone crear nuevos objetos copiando instancias existentes llamadas prototipos. En lugar de instanciar clases directamente con new, se clona un objeto configurado previamente, lo que resulta útil cuando la inicialización es costosa o cuando el tipo concreto debe seleccionarse en tiempo de ejecución.

Aplicar Prototype permite construir catálogos de objetos listos para usar y personalizarlos sin acoplar el código cliente a clases concretas. Es especialmente valioso en sistemas que crean gran cantidad de objetos similares o que requieren preservar configuraciones complejas.

10.1 Problema que resuelve

Instanciar clases con estructuras profundas, dependencias externas u operaciones pesadas puede ser costoso. Además, cuando se reciben objetos desde configuraciones o se desea permitir extensiones por parte del usuario, el código cliente no siempre conoce el tipo exacto a crear.

Prototype permite registrar objetos base y clonarlos cuando se necesiten nuevas instancias. Esto evita repetir inicializaciones costosas y reduce el acoplamiento con las clases concretas. Cada clon puede ajustarse según sea necesario sin afectar el prototipo original.

10.2 Intención y motivación

La intención del patrón es especificar los tipos de objetos a crear mediante una instancia prototípica que se clona. El cliente solicita un prototipo ya configurado y obtiene una copia funcional, sobre la cual puede aplicar cambios adicionales.

Los prototipos pueden registrarse en un repositorio o registry, donde cada entrada representa una configuración del objeto. Esto habilita un catálogo que puede extenderse en tiempo de ejecución sin tocar el código compilado.

10.3 Estructura y participantes

Los roles principales en Prototype son:

  • Prototype: interfaz o clase abstracta que declara el método de clonación.
  • Concrete Prototype: clases que implementan la clonación y devuelven copias de sí mismas.
  • Client: solicita instancias copiando prototipos en lugar de usar new.
  • Prototype Registry (opcional): almacena y administra la colección de prototipos disponibles.

En C#, la clonación puede implementarse recurriendo al método protegido MemberwiseClone, definiendo interfaces como ICloneable o empleando constructores copia. La decisión depende de cuán profunda deba ser la copia.

10.4 Clonación superficial vs. profunda

La clonación superficial copia los valores directos de las propiedades, pero mantiene referencias compartidas a objetos mutables. Es adecuada cuando los colaboradores son inmutables o se comparten de forma intencional.

La clonación profunda crea copias independientes de todos los objetos compuestos que forman parte del prototipo. Asegura que el clon no comparta estado con el original, a costa de mayor complejidad.

Seleccionar la estrategia correcta es crucial para evitar efectos secundarios. Un prototipo que sea modificado por error puede afectar a otros elementos si la clonación es superficial cuando debería ser profunda.

10.5 Implementación en C# con clonación superficial

El siguiente ejemplo en C# muestra un prototipo de credencial. Implementa ICloneable y realiza una copia superficial, por lo que las listas se comparten entre instancias.

using System;
using System.Collections.Generic;

public class Credencial : ICloneable
{
    public Credencial(string nombre, string rol, List<string> permisos)
    {
        Nombre = nombre;
        Rol = rol;
        Permisos = permisos;
    }

    public string Nombre { get; private set; }
    public string Rol { get; }
    public List<string> Permisos { get; }

    public void EstablecerNombre(string nombre)
    {
        Nombre = nombre;
    }

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

Al clonar, la lista de permisos se comparte. Si cada credencial debe mantener sus propios permisos, es necesario pasar a una clonación profunda.

10.6 Clonación profunda con constructor copia

Una alternativa segura consiste en implementar un constructor copia que genere nuevas instancias para los campos mutables.

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;

public class CredencialCompleta
{
    public CredencialCompleta(string nombre, string rol, IEnumerable<string> permisos)
    {
        Nombre = nombre;
        Rol = rol;
        Permisos = new List<string>(permisos);
    }

    private CredencialCompleta(CredencialCompleta original)
    {
        Nombre = original.Nombre;
        Rol = original.Rol;
        Permisos = new List<string>(original.Permisos);
    }

    public string Nombre { get; private set; }
    public string Rol { get; private set; }
    public List<string> Permisos { get; }

    public IReadOnlyList<string> PermisosLectura => new ReadOnlyCollection<string>(Permisos);

    public void EstablecerRol(string rol)
    {
        Rol = rol;
    }

    public CredencialCompleta Clone()
    {
        return new CredencialCompleta(this);
    }
}

El constructor copia asegura que cada clon tenga su propia lista de permisos, evitando que cambios en un clon afecten a otros.

10.7 Ejemplo completo en C#: registro de prototipos

Implementaremos un registro de prototipos que permite obtener distintas configuraciones de vehículos (sedán, SUV, deportivo). Cada prototipo se clona y se personaliza con datos específicos.

using System;
using System.Collections.Generic;

public class Vehiculo
{
    public Vehiculo(string marca, string modelo, string color, Motor motor)
    {
        Marca = marca;
        Modelo = modelo;
        Color = color;
        Motor = motor;
    }

    private Vehiculo(Vehiculo original)
    {
        Marca = original.Marca;
        Modelo = original.Modelo;
        Color = original.Color;
        Motor = original.Motor.Clone();
    }

    public string Marca { get; private set; }
    public string Modelo { get; private set; }
    public string Color { get; private set; }
    public Motor Motor { get; private set; }

    public void EstablecerColor(string color)
    {
        Color = color;
    }

    public void EstablecerModelo(string modelo)
    {
        Modelo = modelo;
    }

    public Vehiculo Clone()
    {
        return new Vehiculo(this);
    }

    public override string ToString()
    {
        return $"{Marca} {Modelo} ({Color}, {Motor})";
    }
}

public class Motor
{
    public Motor(int cilindrada, string tipo)
    {
        Cilindrada = cilindrada;
        Tipo = tipo;
    }

    public int Cilindrada { get; }
    public string Tipo { get; }

    public Motor Clone()
    {
        return new Motor(Cilindrada, Tipo);
    }

    public override string ToString()
    {
        return $"{Cilindrada}cc {Tipo}";
    }
}

public class RegistroVehiculos
{
    private readonly Dictionary<string, Vehiculo> _prototipos = new Dictionary<string, Vehiculo>();

    public void Registrar(string clave, Vehiculo vehiculo)
    {
        _prototipos[clave] = vehiculo;
    }

    public Vehiculo Obtener(string clave)
    {
        if (!_prototipos.TryGetValue(clave, out var prototipo))
        {
            throw new ArgumentException($"No existe prototipo para la clave: {clave}", nameof(clave));
        }

        return prototipo.Clone();
    }
}

El cliente configura el registro con prototipos y obtiene copias personalizadas sin conocer las clases concretas:

using System;

public static class DemoPrototype
{
    public static void Main()
    {
        var registro = new RegistroVehiculos();
        registro.Registrar("sedan", new Vehiculo("AutoYa", "Sedan LX", "gris", new Motor(1600, "nafta")));
        registro.Registrar("suv", new Vehiculo("AutoYa", "SUV Familiar", "negro", new Motor(2200, "diesel")));

        var vehiculoCliente = registro.Obtener("sedan");
        vehiculoCliente.EstablecerColor("azul");
        vehiculoCliente.EstablecerModelo("Sedan LX Plus");

        Console.WriteLine(vehiculoCliente);
    }
}

El prototipo original permanece intacto en el registro, mientras que el cliente obtiene una copia independiente que puede modificar según sus preferencias.

Prototype

10.8 Ventajas y beneficios

  • Reduce el impacto de crear objetos costosos al reutilizar instancias configuradas.
  • Permite agregar tipos en tiempo de ejecución simplemente registrando nuevos prototipos.
  • Evita acoplar el cliente a clases concretas; basta con trabajar con la interfaz de clonación.
  • Facilita la configuración en herramientas visuales o archivos, ya que los prototipos pueden ser definidos externamente.

10.9 Desventajas y cautelas

La clonación puede ser compleja si el objeto tiene dependencias no clonables o recursos compartidos. Es necesario decidir qué partes se copian y cuáles se referencian, lo que aumenta el esfuerzo de mantenimiento. Además, el uso de ICloneable en C# requiere cuidado porque devuelve object y suele complementarse con métodos tipados o constructores copia para evitar conversiones inseguras.

En escenarios donde la inicialización es trivial o el número de combinaciones es reducido, Prototype puede resultar innecesario. Evaluar siempre la relación costo-beneficio antes de implementar la clonación.

10.10 Cuándo elegir Prototype

Considere este patrón cuando:

  • El costo de crear objetos desde cero es alto y conviene reutilizar configuraciones base.
  • Se necesita crear objetos en tiempo de ejecución sin conocer su tipo concreto de antemano.
  • Se requiere un catálogo de configuraciones que pueda ampliarse sin recompilar.
  • Los objetos deben clonarse frecuentemente y mantener independencia entre sus copias.

Prototype complementa a Abstract Factory y Builder: una fábrica puede devolver prototipos precargados y un builder puede iniciarse a partir de un prototipo para aplicar ajustes finales.