🧬 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.
¿Cuándo debería usarlo?
El patrón de diseño Prototype brilla en en las siguientes situaciones:
- Cuando crear un objeto es costoso: Si la inicialización de un objeto implica consultas a bases de datos, cálculos complejos o configuraciones elaboradas, clonar puede ser mucho más eficiente.
- 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 mantener un catálogo de objetos prototipo: Puedes tener un registro de objetos base y clonar el que necesites en cada momento.
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:
- Rendimiento: Clonar objetos complejos es más rápido que crearlos desde cero
- Flexibilidad: Puedes crear variantes de objetos fácilmente
- Reduce código duplicado: No necesitas múltiples constructores o métodos de inicialización
- Independencia de clases concretas: El código cliente trabaja con prototipos sin conocer sus clases específicas
Desventajas:
- Clonación profunda puede ser compleja: Objetos con muchas referencias requieren cuidado al clonar
- Confusión con referencias: Hay que tener cuidado con objetos que contengan referencias a otros objetos
- No siempre es necesario: Para objetos simples, usar el constructor es más claro
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! 🍻