🚨 ¡Nueva review! ¡Mi teclado ideal! ⌨️ Perfecto para programar, el Logitech MX Keys S . ¡Échale un ojo! 👀

Patrón de diseño: Prototype

Clonando objetos like a pro

Escrito por domin el 02/10/2001

🧬 Patrón de diseño: Prototype

¿Qué es?

El patrón Prototype es un patrón de diseño del tipo creacional que permite crear nuevos objetos clonando instancias existentes, en lugar de construirlos desde cero usando constructores tradicionales.

Por ejemplo, imagina que tienes un objeto complejo que te ha costado mucho configurar con un montón de propiedades y dependencias e historias varias. En lugar de volver a pasar por todo ese calvario de proceso cada vez que necesites uno parecido, simplemente clonas el que ya tienes y haces los ajustes necesarios y chinpum.

Es como tener una plantilla de la que puedes copiarte cuantas veces necesites, modificando solo lo que necesites en cada copia.

Builder Pattern

¿Cuándo debería usarlo?

El patrón de diseño Prototype brilla en en las siguientes situaciones:

Implementación básica en PHP

PHP tiene soporte nativo para la clonación de objetos con la palabra clave clone y el método mágico __clone().

<?php

class Documento {
    private string $titulo;
    private string $contenido;
    private array $metadatos;
    private DateTime $fechaCreacion;
    
    public function __construct(string $titulo, string $contenido) {
        $this->titulo = $titulo;
        $this->contenido = $contenido;
        $this->metadatos = [];
        $this->fechaCreacion = new DateTime();
    }
    
    // Método mágico para personalizar la clonación
    public function __clone() {
        // Clonación profunda: creamos nuevas instancias de objetos
        $this->fechaCreacion = clone $this->fechaCreacion;
        // Los arrays se clonan automáticamente
    }
    
    public function setTitulo(string $titulo): void {
        $this->titulo = $titulo;
    }
    
    public function agregarMetadato(string $clave, string $valor): void {
        $this->metadatos[$clave] = $valor;
    }
    
    public function mostrarInfo(): void {
        echo "Título: {$this->titulo}\n";
        echo "Contenido: {$this->contenido}\n";
        echo "Fecha: {$this->fechaCreacion->format('Y-m-d H:i:s')}\n";
        echo "Metadatos: " . json_encode($this->metadatos) . "\n\n";
    }
}

// Uso del patrón
$documentoOriginal = new Documento(
    "Contrato Base",
    "Este es el contenido estándar del contrato..."
);
$documentoOriginal->agregarMetadato("tipo", "contrato");
$documentoOriginal->agregarMetadato("departamento", "legal");

echo "=== Documento Original ===\n";
$documentoOriginal->mostrarInfo();

// Clonamos el documento
$documentoCliente1 = clone $documentoOriginal;
$documentoCliente1->setTitulo("Contrato - Cliente ABC");
$documentoCliente1->agregarMetadato("cliente", "ABC Corp");

echo "=== Documento Clonado ===\n";
$documentoCliente1->mostrarInfo();

Ejemplo práctico: Sistema de productos

Vamos con un ejemplo más realista de un sistema de e-commerce donde tenemos productos con configuraciones complejas:

<?php

class Producto {
    private string $nombre;
    private float $precio;
    private array $caracteristicas;
    private Imagen $imagenPrincipal;
    
    public function __construct(string $nombre, float $precio) {
        $this->nombre = $nombre;
        $this->precio = $precio;
        $this->caracteristicas = [];
        $this->imagenPrincipal = new Imagen("default.jpg");
    }
    
    public function __clone() {
        // Clonación profunda de objetos anidados
        $this->imagenPrincipal = clone $this->imagenPrincipal;
        // Creamos una copia del array de características
        $this->caracteristicas = array_map(function($item) {
            return is_object($item) ? clone $item : $item;
        }, $this->caracteristicas);
    }
    
    public function setNombre(string $nombre): void {
        $this->nombre = $nombre;
    }
    
    public function setPrecio(float $precio): void {
        $this->precio = $precio;
    }
    
    public function agregarCaracteristica(string $clave, $valor): void {
        $this->caracteristicas[$clave] = $valor;
    }
    
    public function setImagen(Imagen $imagen): void {
        $this->imagenPrincipal = $imagen;
    }
    
    public function mostrar(): string {
        return sprintf(
            "Producto: %s | Precio: %.2f€ | Características: %d",
            $this->nombre,
            $this->precio,
            count($this->caracteristicas)
        );
    }
}

class Imagen {
    private string $ruta;
    private array $filtros;
    
    public function __construct(string $ruta) {
        $this->ruta = $ruta;
        $this->filtros = [];
    }
    
    public function setRuta(string $ruta): void {
        $this->ruta = $ruta;
    }
}

// Catálogo de prototipos
class CatalogoProductos {
    private array $prototipos = [];
    
    public function registrarPrototipo(string $clave, Producto $prototipo): void {
        $this->prototipos[$clave] = $prototipo;
    }
    
    public function crearProducto(string $clave): ?Producto {
        if (!isset($this->prototipos[$clave])) {
            return null;
        }
        
        // Clonamos el prototipo
        return clone $this->prototipos[$clave];
    }
}

// Uso del sistema
$catalogo = new CatalogoProductos();

// Creamos el prototipo de portátil
$prototipoPortatil = new Producto("Portátil Base", 599.99);
$prototipoPortatil->agregarCaracteristica("tipo", "portátil");
$prototipoPortatil->agregarCaracteristica("garantia", "2 años");
$prototipoPortatil->agregarCaracteristica("envio", "gratuito");

// Registramos el prototipo
$catalogo->registrarPrototipo("portatil-base", $prototipoPortatil);

// Creamos variantes clonando el prototipo
$portatilGaming = $catalogo->crearProducto("portatil-base");
$portatilGaming->setNombre("Portátil Gaming RTX");
$portatilGaming->setPrecio(1299.99);
$portatilGaming->agregarCaracteristica("gpu", "RTX 4060");
$portatilGaming->agregarCaracteristica("ram", "32GB");

$portatilOficina = $catalogo->crearProducto("portatil-base");
$portatilOficina->setNombre("Portátil Oficina Pro");
$portatilOficina->setPrecio(799.99);
$portatilOficina->agregarCaracteristica("peso", "1.2kg");

echo $portatilGaming->mostrar() . "\n";
echo $portatilOficina->mostrar() . "\n";

Otro ejemplo: Sistema de notificaciones

Por si no queda claro, aquí va otro ejemplo:

<?php

class ConfiguracionNotificacion
{
    private array $estilos = [];
    private array $adjuntos = [];
    private Remitente $remitente;

    public function __construct(
        private string $asunto,
        private string $plantillaHtml,
        ?Remitente $remitente = null
    ) {
        $this->remitente = $remitente ?? new Remitente("noreply@empresa.com", "Sistema");
    }

    public function __clone(): void
    {
        $this->remitente = clone $this->remitente;
    }

    public function setAsunto(string $asunto): void
    {
        $this->asunto = $asunto;
    }

    public function personalizarContenido(array $variables): void
    {
        $this->plantillaHtml = strtr(
            $this->plantillaHtml,
            array_combine(
                array_map(fn($k) => "{{{$k}}}", array_keys($variables)),
                array_values($variables)
            )
        );
    }

    public function agregarAdjunto(string $archivo): void
    {
        $this->adjuntos[] = $archivo;
    }

    public function enviar(string $destinatario): void
    {
        echo <<<MSG
        📧 Enviando notificación a: {$destinatario}
           Asunto: {$this->asunto}
           Adjuntos: {count($this->adjuntos)}
           Contenido: {substr($this->plantillaHtml, 0, 50)}...

        MSG;
    }
}


class Remitente {
    public function __construct(
        private string $email,
        private string $nombre
    ) {}
}

// Sistema de notificaciones
class GestorNotificaciones
{
    /** @var array<string, ConfiguracionNotificacion> */
    private array $plantillas = [];

    public function __construct(private readonly string $tipoDefecto = '') {}

    public function registrarPlantilla(string $tipo, ConfiguracionNotificacion $config): void
    {
        $this->plantillas[$tipo] = $config;
    }

    public function crearNotificacion(string $tipo): ?ConfiguracionNotificacion
    {
        return $this->plantillas[$tipo] ?? null
            ? clone $this->plantillas[$tipo]
            : null;
    }
}


// Uso del sistema
$gestor = new GestorNotificaciones();

// Creamos la plantilla base de bienvenida
$plantillaBienvenida = new ConfiguracionNotificacion(
    "¡Bienvenido a nuestra plataforma!",
    "<html><body><h1>Hola {{nombre}}</h1><p>{{mensaje}}</p></body></html>"
);
$plantillaBienvenida->agregarAdjunto("guia-usuario.pdf");

$gestor->registrarPlantilla("bienvenida", $plantillaBienvenida);

// Creamos notificaciones personalizadas para diferentes usuarios
$usuarios = [
    ["nombre" => "Ana García", "email" => "ana@example.com", "mensaje" => "Gracias por registrarte en nuestra app de fitness"],
    ["nombre" => "Carlos López", "email" => "carlos@example.com", "mensaje" => "Estamos encantados de tenerte con nosotros"],
    ["nombre" => "María Ruiz", "email" => "maria@example.com", "mensaje" => "Tu cuenta premium está lista para usar"]
];

foreach ($usuarios as $usuario) {
    $notificacion = $gestor->crearNotificacion("bienvenida");
    $notificacion->personalizarContenido([
        "nombre" => $usuario["nombre"],
        "mensaje" => $usuario["mensaje"]
    ]);
    $notificacion->enviar($usuario["email"]);
}

// Salida:
// 📧 Enviando notificación a: ana@example.com
//    Asunto: ¡Bienvenido a nuestra plataforma!
//    Adjuntos: 1
//    Contenido: <html><body><h1>Hola Ana García</h1><p>blablabla...
//
// 📧 Enviando notificación a: carlos@example.com
//    Asunto: ¡Bienvenido a nuestra plataforma!
//    Adjuntos: 1
//    Contenido: <html><body><h1>Hola Carlos López</h1><p>blablabla...
//
// 📧 Enviando notificación a: maria@example.com
//    Asunto: ¡Bienvenido a nuestra plataforma!
//    Adjuntos: 1
//    Contenido: <html><body><h1>Hola María Ruiz</h1><p>blablabla...

Ventajas y desventajas

Ventajas:

Desventajas:

Conclusión

El patrón Prototype es especialmente útil cuando trabajas con objetos que requieren configuraciones complejas o cuando necesitas crear muchas variantes de un mismo tipo de objeto. PHP hace que implementarlo sea bastante sencillo con su soporte nativo para clonación.

Recuerda que no siempre es la mejor solución: si tus objetos son simples y económicos de crear, probablemente no necesites este patrón. Como siempre, úsalo cuando realmente aporte valor a tu diseño.

¡Un saludo y nos vemos en los bares! 🍻