🧬 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.

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().
La interfaz que declara el método clone. En PHP no necesitas declararla explícitamente porque clone es nativo del lenguaje.
Las clases que implementan la lógica de clonación. Definen __clone() para personalizar qué pasa al copiar.
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):
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.
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:
- Cuando crear un objeto es costoso: Si la inicialización implica consultas a bases de datos, llamadas a APIs, cálculos complejos o configuraciones elaboradas, clonar es mucho más rápido.
- Cuando los objetos tienen muchas configuraciones posibles: Si tienes objetos con múltiples variantes que comparten la mayoría de sus propiedades.
- Cuando quieres evitar subclases para cada variante: En lugar de crear una clase para cada tipo de objeto, clonas y modificas.
- Cuando necesitas un catálogo de objetos prototipo: Puedes tener un registro de objetos base y clonar el que necesites en cada momento.
¿Cuándo NO usarlo? ⚠️
- Objetos simples: Si el constructor tiene 2-3 parámetros y no hay configuración compleja, un
newde toda la vida es más claro. - Objetos con dependencias circulares: Si A referencia a B y B referencia a A, la clonación profunda se convierte en un infierno de recursión infinita.
- Cuando la inmutabilidad importa: Si tus objetos son inmutables (no cambian después de crearse), clonar no tiene sentido. Simplemente reutiliza la misma instancia.
¿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 🆚
| Aspecto | Prototype | Factory Method | Builder |
|---|---|---|---|
| ¿Cómo crea? | Clonando un objeto existente | Delegando a una subclase | Paso a paso con un builder |
| ¿Cuándo usarlo? | Crear desde cero es costoso | No sabes qué tipo crear hasta runtime | Muchos parámetros opcionales |
| Complejidad | Baja (si no hay deep copy complejo) | Media | Media-Alta |
| Ejemplo rápido | Copiar un documento y modificarlo | Crear 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
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.
El código cliente trabaja con el catálogo y las interfaces, no necesita conocer las clases concretas de los prototipos.
El prototipo se encarga de saber cómo clonarse. El catálogo se encarga de almacenar y servir prototipos. Cada uno a lo suyo.
Cualquier objeto que implemente la interfaz del prototipo puede ser clonado de la misma forma. El cliente no distingue entre prototipos.
Ventajas y Desventajas ⚖️
- 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.
- 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:
- Confirmación de pedido: con asunto, cuerpo HTML, adjunto de factura y remitente “pedidos@tienda.com”
- Envío realizado: con asunto, cuerpo HTML, tracking number y remitente “envios@tienda.com”
- Crea un
RegistroDePlantillasque almacene ambas plantillas. - Clona la plantilla de “confirmación de pedido” para 3 clientes diferentes, personalizando el nombre y número de pedido.
- Comprueba que modificar la notificación de un cliente no afecta a las demás ni al prototipo original.
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! 🍻