🚨 ¡Nueva review! ✨ Mi ratón favorito para programar: el Logitech MX Master 3S . ¡Échale un ojo! 👀

Patrón de diseño: Prototype

Clonando objetos like a pro

Escrito por domin el 2 de octubre de 2025 · Actualizado el 8 de febrero de 2026

🧬 Patrón de diseño: Prototype

El patrón de diseño Prototype es patrón del tipo creacional del catálogo Gang of Four (GoF) que permite crear nuevos objetos clonando instancias existentes, en lugar de construirlos desde cero usando constructores tradicionales.

Diagrama del patrón de diseño Prototype

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.

¿Qué tipo de patrón de diseño es el Prototype?


¿Cómo funciona?

La idea es simple: en lugar de hacer new MiClase() y configurar todo desde cero, haces clone $objetoExistente y solo cambias lo que sea diferente. PHP tiene soporte nativo para esto con la palabra clave clone y el método mágico __clone().

🧬 Prototype

La interfaz que declara el método clone. En PHP no necesitas declararla explícitamente porque clone es nativo del lenguaje.

📦 Concrete Prototype

Las clases que implementan la lógica de clonación. Definen __clone() para personalizar qué pasa al copiar.

📋 Registry

Opcional. Un catálogo que almacena prototipos base y te permite clonar el que necesites por clave. Muy útil en la práctica.


Shallow Copy vs. Deep Copy

Esto es lo más importante del patrón Prototype y donde más gente la lía. Cuando haces clone en PHP, por defecto se hace una copia superficial (shallow copy):

Shallow Copy (por defecto)

Las propiedades escalares (strings, ints, bools) se copian. Pero los objetos anidados NO se copian, solo se copia la referencia. Modificar un objeto anidado en el clon afecta al original.

Deep Copy (con __clone)

Usas __clone() para clonar también los objetos anidados. Cada clon es totalmente independiente del original. Es lo que quieres el 99% de las veces.

Veamos el problema con un ejemplo:

<?php

class Direccion {
    public function __construct(
        public string $calle,
        public string $ciudad
    ) {}
}

class Persona {
    public function __construct(
        public string $nombre,
        public Direccion $direccion
    ) {}
}

$original = new Persona("Paco", new Direccion("Gran Vía 1", "Madrid"));
$clon = clone $original;
$clon->nombre = "Pepe";
$clon->direccion->ciudad = "Barcelona";

echo $original->direccion->ciudad; // "Barcelona" 💀
// ¡¡La dirección del original TAMBIÉN ha cambiado!!

Paco se ha mudado a Barcelona sin enterarse. Esto pasa porque clone por defecto solo copia la referencia al objeto Direccion, no crea uno nuevo. Para arreglar esto necesitas el __clone():

<?php

class Persona {
    public function __construct(
        public string $nombre,
        public Direccion $direccion
    ) {}

    public function __clone(): void {
        // Clonación profunda: creamos una nueva Direccion
        $this->direccion = clone $this->direccion;
    }
}

$original = new Persona("Paco", new Direccion("Gran Vía 1", "Madrid"));
$clon = clone $original;
$clon->nombre = "Pepe";
$clon->direccion->ciudad = "Barcelona";

echo $original->direccion->ciudad; // "Madrid" ✅
// Ahora sí, cada uno tiene su dirección independiente

Si tu clase tiene propiedades que son objetos, necesitas __clone() para hacer deep copy. Si solo tiene escalares y arrays simples, el clone por defecto te vale.

👀 Los arrays en PHP se copian por valor, así que se clonan automáticamente. Pero si el array contiene objetos, esos objetos siguen siendo referencias. Toca clonarlos uno a uno dentro de __clone().

¿Qué hace PHP por defecto cuando usas 'clone' en un objeto?


¿Cuándo debería usarlo?

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

¿Cuándo NO usarlo? ⚠️

¿Cuándo NO deberías usar el patrón Prototype?


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 (a nivel superficial)
    }

    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();

Fíjate en que el documento clonado conserva los metadatos del original (“tipo” y “departamento”) pero le podemos añadir otros nuevos sin tocar el original. Si no hubiéramos clonado el DateTime en __clone(), ambos documentos compartirían la misma fecha y cambiar una cambiaría la otra.


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 y un catálogo de prototipos (el Registry del patrón):

<?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;
        // Clonamos objetos dentro 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 (Registry)
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";
// Producto: Portátil Gaming RTX | Precio: 1299.99€ | Características: 5

echo $portatilOficina->mostrar() . "\n";
// Producto: Portátil Oficina Pro | Precio: 799.99€ | Características: 4

Lo bueno del catálogo es que te permite tener “plantillas” registradas y clonar la que necesites con una sola línea. El portátil gaming y el de oficina comparten la base (garantía, envío gratuito, tipo portátil) pero cada uno tiene sus características propias.

¿Para qué sirve el método mágico __clone() en PHP?


Otro ejemplo: Sistema de notificaciones

Por si no queda claro, aquí va otro ejemplo con plantillas de emails:

<?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
    {
        $numAdjuntos = count($this->adjuntos);
        $preview = substr($this->plantillaHtml, 0, 50);
        echo "📧 Enviando a: {$destinatario}\n";
        echo "   Asunto: {$this->asunto}\n";
        echo "   Adjuntos: {$numAdjuntos}\n";
        echo "   Contenido: {$preview}...\n\n";
    }
}

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

// Gestor con registro de plantillas (Registry)
class GestorNotificaciones
{
    /** @var array<string, ConfiguracionNotificacion> */
    private array $plantillas = [];

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

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

        return clone $this->plantillas[$tipo];
    }
}

// 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 clonando la plantilla
$usuarios = [
    ["nombre" => "Ana García", "email" => "ana@example.com", "mensaje" => "Gracias por registrarte"],
    ["nombre" => "Carlos López", "email" => "carlos@example.com", "mensaje" => "Bienvenido a bordo"],
    ["nombre" => "María Ruiz", "email" => "maria@example.com", "mensaje" => "Tu cuenta premium está lista"]
];

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

Cada notificación se personaliza de forma independiente. Si modificas la de Ana, la de Carlos no se entera. Eso es gracias a la clonación profunda del Remitente en __clone().

¿Qué es el Registry (catálogo) en el patrón Prototype?


Prototype vs. Otros Patrones Creacionales 🆚

AspectoPrototypeFactory MethodBuilder
¿Cómo crea?Clonando un objeto existenteDelegando a una subclasePaso a paso con un builder
¿Cuándo usarlo?Crear desde cero es costosoNo sabes qué tipo crear hasta runtimeMuchos parámetros opcionales
ComplejidadBaja (si no hay deep copy complejo)MediaMedia-Alta
Ejemplo rápidoCopiar un documento y modificarloCrear notificaciones (email, SMS)Montar una hamburguesa ingrediente a ingrediente

Si ya tienes un objeto bien configurado y necesitas variantes parecidas, usa Prototype. Si necesitas decidir qué tipo crear, usa Factory Method. Si la construcción tiene muchos pasos opcionales, usa Builder.

¿Cuál es la diferencia principal entre Prototype y Factory Method?


Relación con los Principios SOLID

OCP (Open/Closed)

Puedes introducir nuevos prototipos en el registro sin modificar el código que los usa. Nuevas variantes = nuevo prototipo registrado, cero cambios en el cliente.

DIP (Dependency Inversion)

El código cliente trabaja con el catálogo y las interfaces, no necesita conocer las clases concretas de los prototipos.

SRP (Single Responsibility)

El prototipo se encarga de saber cómo clonarse. El catálogo se encarga de almacenar y servir prototipos. Cada uno a lo suyo.

LSP (Liskov Substitution)

Cualquier objeto que implemente la interfaz del prototipo puede ser clonado de la misma forma. El cliente no distingue entre prototipos.


Ventajas y Desventajas ⚖️

Ventajas
  • Rendimiento: Clonar objetos complejos es más rápido que crearlos desde cero.
  • Flexibilidad: Puedes crear variantes fácilmente sin subclases.
  • Reduce código duplicado: No necesitas múltiples constructores ni métodos de inicialización.
  • Independencia de clases concretas: El código cliente trabaja con prototipos sin conocer sus clases específicas.
  • Catálogo de plantillas: El Registry permite tener prototipos pre-configurados listos para clonar.
Desventajas
  • Deep copy compleja: Objetos con muchas referencias anidadas requieren mucho cuidado en __clone().
  • Dependencias circulares: Si A referencia a B y B a A, la clonación profunda puede entrar en bucle infinito.
  • No siempre necesario: Para objetos simples, un constructor es más claro y directo.
  • Difícil de depurar: Si te olvidas de clonar un objeto anidado, el bug puede ser muy sutil y aparecer tarde.

Errores comunes al implementarlo

Estos son los tropezones más habituales que he visto con este patrón:

1. Olvidarse del deep copy: El clásico. Haces clone y te crees que todo va bien, pero dos objetos comparten una referencia interna sin que te des cuenta. El bug aparece mucho después y es difícil de rastrear.

2. No clonar objetos dentro de arrays: Los arrays se copian por valor en PHP, pero los objetos que contienen siguen siendo referencias. Si tu array tiene objetos, tienes que recorrerlo y clonar cada uno en __clone().

// ❌ Mal: los objetos dentro del array siguen siendo los mismos
public function __clone() {
    // $this->items ya es una copia del array, pero los objetos dentro no
}

// ✅ Bien: clonamos cada objeto del array
public function __clone() {
    $this->items = array_map(function($item) {
        return is_object($item) ? clone $item : $item;
    }, $this->items);
}

Si clonas un objeto que tiene un array con objetos dentro y NO implementas __clone(), ¿qué pasa?


3. Usar Prototype cuando un constructor basta: Si tu objeto tiene 3 propiedades simples, no necesitas Prototype. Es añadir complejidad gratis. Un new de toda la vida es más legible y más fácil de mantener.

4. No pensar en la identidad: Cuando clonas un objeto, ¿el clon debería tener el mismo ID que el original? Normalmente no. Acuérdate de resetear campos como IDs, timestamps de creación o cualquier cosa que deba ser única.

public function __clone() {
    $this->id = null;  // El clon no debería tener el ID del original
    $this->fechaCreacion = new DateTime();  // Nueva fecha para el clon
}

Al clonar un objeto, ¿qué campos suele ser buena idea resetear en __clone()?


Ejercicio Práctico 🧠

Para que practiques el patrón Prototype, te propongo este ejercicio:

Tu tarea: Crear un sistema de plantillas de correos para una tienda online con estas plantillas base:

  1. Confirmación de pedido: con asunto, cuerpo HTML, adjunto de factura y remitente “pedidos@tienda.com
  2. Envío realizado: con asunto, cuerpo HTML, tracking number y remitente “envios@tienda.com

Si al modificar el clon de un cliente los demás no se enteran, has implementado correctamente el deep copy y dominas el patrón Prototype.


¡Un saludo y nos vemos en los bares! 🍻