Teoría
Definición 🧬
El patrón Prototype (Prototipo) es un patrón de diseño creacional que te permite crear nuevos objetos copiando una instancia existente, en lugar de crear el objeto desde cero. A esta instancia que se copia se le llama “prototipo”.
Funciona declarando un método de clonación en una interfaz común. Todas las clases que soportan la clonación pueden ser copiadas para crear nuevos objetos con el mismo estado inicial, que luego pueden ser modificados.
Problema
Imagina que tienes un objeto que es muy costoso de crear. Su instanciación podría requerir consultas a la base de datos, llamadas a servicios de red, cálculos complejos o la lectura de archivos de configuración pesados.
Si necesitas crear muchas instancias de este objeto, o instancias que son muy similares entre sí, repetir este costoso proceso de creación una y otra vez es muy ineficiente. Por ejemplo, cargar la configuración de un objeto Reporte con su logo, pie de página y estilos corporativos cada vez que se genera un nuevo reporte.
Solución
En lugar de construir el objeto desde cero, el patrón Prototype propone una solución más directa:
- Crear un objeto prototipo: Se crea una instancia inicial del objeto con la configuración base (por ejemplo, el
Reportecorporativo). - Clonar el prototipo: Cuando se necesita un nuevo objeto, en lugar de usar el operador
newy reconfigurar todo, se le pide al prototipo que se clone a sí mismo. - Modificar el clon: El nuevo objeto es una copia exacta del prototipo, pero es una instancia completamente independiente. Ahora puedes modificar los detalles específicos (como el título o el contenido) del clon sin afectar al prototipo original.
Esto es especialmente útil cuando la mayoría de los objetos que necesitas son variaciones de un mismo “molde”.
Estructura (Mermaid UML)
El diagrama es simple: un cliente pide a un prototipo que se clone, y recibe un nuevo objeto del mismo tipo.
classDiagram direction TB
class Client note for Client "Pide un clon a un prototipo"
class Prototype { <<Interface>> +clone(): Prototype }
class ConcretePrototype1 { -field1 +clone(): Prototype }
class ConcretePrototype2 { -field2 +clone(): Prototype }
Client ..> Prototype : solicita clon Prototype <|.. ConcretePrototype1 Prototype <|.. ConcretePrototype2A menudo, se utiliza un Registro de Prototipos (Prototype Registry), que es una clase que gestiona un conjunto de prototipos listos para ser clonados bajo demanda.
Cuándo usar
- Cuando la creación de un objeto es costosa y es más eficiente copiar uno existente.
- Cuando tu código no debe depender de las clases concretas de los objetos que necesitas copiar.
- Cuando quieres ofrecer un conjunto de “objetos predefinidos” que los clientes puedan configurar.
Cuándo no usar
- Cuando los objetos son simples, no tienen un estado complicado y su creación no consume muchos recursos.
- Si tu objeto tiene muchas referencias circulares o una lógica de clonación muy compleja que podría generar errores.
Ejemplo en Spring Boot (Java)
// Interfaz Prototypepublic interface DocumentoPrototype extends Cloneable { DocumentoPrototype clonar(); void setContenido(String contenido);}
// Prototipo Concretopublic class Contrato implements DocumentoPrototype { private String tipo; private String contenidoBase; // Contenido costoso de cargar
public Contrato() { this.tipo = "Contrato de Confidencialidad"; // Imagina que esto se carga de un archivo o DB this.contenidoBase = "Este es el texto legal base..."; }
@Override public DocumentoPrototype clonar() { return new Contrato(); // Clonación simple en este caso }
@Override public void setContenido(String contenido) { this.contenidoBase = contenido; }}
// Cliente@Servicepublic class GestorDocumentos { public void generarDocumentos() { Contrato prototipoContrato = new Contrato();
// Creamos un nuevo contrato clonando el prototipo Contrato nuevoContrato = (Contrato) prototipoContrato.clonar(); nuevoContrato.setContenido("...añadiendo cláusulas específicas para el empleado X.");
// El prototipo original no ha cambiado }}Ejemplo en Django (Python)
import copy
# Prototipo (puede ser una clase base o no)class Documento: def __init__(self, titulo, contenido_base): self.titulo = titulo self.contenido = contenido_base # Imagina que contenido_base es costoso de generar
def clonar(self): # deepcopy crea una copia completamente independiente return copy.deepcopy(self)
# Cliente (una vista de Django)from django.http import JsonResponse
# Registro de PrototiposPROTOTIPOS = { "contrato": Documento("Contrato de Empleado", "Texto legal base..."), "informe": Documento("Informe Mensual", "Estructura base del informe...")}
def crear_documento_view(request, tipo: str): prototipo = PROTOTIPOS.get(tipo) if not prototipo: return JsonResponse({"error": "Tipo de documento no válido"}, status=400)
# Clonamos el prototipo nuevo_documento = prototipo.clonar()
# Modificamos el clon nuevo_documento.contenido += "\nSección añadida para el departamento de ventas."
return JsonResponse(nuevo_documento.__dict__)Resumen
- El patrón Prototype se basa en clonar objetos existentes para crear otros nuevos.
- Es una alternativa a la creación de objetos mediante
newcuando esta es computacionalmente costosa. - Permite crear objetos sin acoplar el código cliente a sus clases concretas, ya que el cliente solo necesita saber cómo clonar.
Práctica con Spring Boot
Paso 1: Creación del Proyecto en IntelliJ IDEA 🚀
- Abre IntelliJ IDEA y ve a File > New > Project….
- Selecciona Spring Initializr.
- Configura los metadatos:
- Name:
prototype-ejemplo - Language: Java
- Type: Gradle - Groovy
- Group:
com.example.solid - JDK: 17 o superior
- Name:
- Haz clic en Next.
- Añade la dependencia Spring Web.
- Haz clic en Create.
Paso 2: Estructura de Paquetes 📂
Dentro de src/main/java/com/example/solid/prototypeejemplo, crea estos paquetes:
model: Contendrá nuestras clases de documentos que actuarán como prototipos.registry: Contendrá una clase para gestionar y servir los prototipos.controller: Contendrá nuestroRestControllerque actuará como cliente.
Paso 3: Codificación del Patrón Prototype 🧬
3.1. Crear los Prototipos
Dentro del paquete model, crea la clase base y las implementaciones concretas.
Documento.java (Prototipo Abstracto)
package com.example.solid.prototypeejemplo.model;
import lombok.Data;
@Data // De Lombok para getters/setterspublic abstract class Documento implements Cloneable { private String titulo; private String contenido; private String firma;
@Override public Documento clone() { try { return (Documento) super.clone(); } catch (CloneNotSupportedException e) { // Esto no debería pasar, ya que implementamos Cloneable return null; } }}Contrato.java (Prototipo Concreto)
package com.example.solid.prototypeejemplo.model;
public class Contrato extends Documento { // Puedes añadir campos específicos del contrato si quieres}Informe.java (Prototipo Concreto)
package com.example.solid.prototypeejemplo.model;
public class Informe extends Documento { // Puedes añadir campos específicos del informe si quieres}3.2. Crear el Registro de Prototipos
Dentro del paquete registry, crea una clase que cargue y gestione los prototipos.
DocumentoRegistry.java
package com.example.solid.prototypeejemplo.registry;
import com.example.solid.prototypeejemplo.model.Contrato;import com.example.solid.prototypeejemplo.model.Documento;import com.example.solid.prototypeejemplo.model.Informe;import jakarta.annotation.PostConstruct;import org.springframework.stereotype.Component;
import java.util.HashMap;import java.util.Map;
@Componentpublic class DocumentoRegistry {
private final Map<String, Documento> prototipos = new HashMap<>();
@PostConstruct // Este método se ejecuta cuando Spring crea el bean public void cargarPrototipos() { // Creamos y configuramos nuestros prototipos base una sola vez Contrato contratoBase = new Contrato(); contratoBase.setTitulo("Contrato de Confidencialidad Estándar"); contratoBase.setContenido("Texto legal base que es costoso de cargar..."); contratoBase.setFirma("Firma Digital de la Empresa");
Informe informeBase = new Informe(); informeBase.setTitulo("Plantilla de Informe Mensual"); informeBase.setContenido("Estructura base del informe..."); informeBase.setFirma("Firma del Departamento de BI");
prototipos.put("contrato", contratoBase); prototipos.put("informe", informeBase); }
public Documento crearDocumento(String tipo) { Documento prototipo = prototipos.get(tipo); if (prototipo != null) { return prototipo.clone(); // ¡Devolvemos un clon, no el original! } return null; }}Paso 4: Crear el Cliente (Controlador REST) 🌐
Dentro del paquete controller, crea el RestController.
DocumentoController.java
package com.example.solid.prototypeejemplo.controller;
import com.example.solid.prototypeejemplo.model.Documento;import com.example.solid.prototypeejemplo.registry.DocumentoRegistry;import org.springframework.http.ResponseEntity;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.PathVariable;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RequestParam;import org.springframework.web.bind.annotation.RestController;
@RestController@RequestMapping("/api/documentos")public class DocumentoController {
private final DocumentoRegistry registry;
public DocumentoController(DocumentoRegistry registry) { this.registry = registry; }
@GetMapping("/crear/{tipo}") public ResponseEntity<Documento> crearDocumento(@PathVariable String tipo, @RequestParam String autor) { Documento nuevoDocumento = registry.crearDocumento(tipo);
if (nuevoDocumento == null) { return ResponseEntity.badRequest().build(); }
// Modificamos el clon con datos específicos de la petición nuevoDocumento.setTitulo(nuevoDocumento.getTitulo() + " para " + autor); nuevoDocumento.setFirma(autor);
return ResponseEntity.ok(nuevoDocumento); }}Paso 5: Probar la Aplicación ✅
- Ejecuta la aplicación desde
PrototypeEjemploApplication. - Usa tu navegador o
curlpara probar los endpoints:
-
Crear un contrato para “Ángel”:
http://localhost:8080/api/documentos/crear/contrato?autor=AngelRespuesta esperada (JSON):
{"titulo": "Contrato de Confidencialidad Estándar para Angel","contenido": "Texto legal base que es costoso de cargar...","firma": "Angel"} -
Crear un informe para “Jesús”:
http://localhost:8080/api/documentos/crear/informe?autor=JesusRespuesta esperada (JSON):
{"titulo": "Plantilla de Informe Mensual para Jesus","contenido": "Estructura base del informe...","firma": "Jesus"}
Como puedes ver, cada petición genera un nuevo objeto basado en el prototipo, pero con sus propias modificaciones.
Práctica con Django (Python)
Paso 1: Creación del Proyecto en PyCharm 🚀
- En PyCharm, ve a File > New Project… y selecciona Django.
- Nombra el proyecto
prototype_djangoy crea una app inicialcore.
Paso 2: Estructura de la App Django 📂
- En la terminal, crea una nueva app:
Terminal window python manage.py startapp documentos - Añade
'documentos'aINSTALLED_APPSenprototype_django/settings.py.
Paso 3: Codificación del Patrón Prototype 🧬
3.1. Crear los Prototipos
En Python, la clonación es más sencilla con el módulo copy.
documentos/models.py (Usaremos este archivo aunque no sean modelos de DB)
import copyfrom dataclasses import dataclass, field
@dataclassclass Documento: titulo: str = "" contenido: str = "" firma: str = ""
def clonar(self): # deepcopy crea una copia recursiva y completamente independiente return copy.deepcopy(self)
@dataclassclass Contrato(Documento): pass
@dataclassclass Informe(Documento): pass3.2. Crear el Registro de Prototipos
documentos/registry.py
from .models import Contrato, Informe, Documento
class DocumentoRegistry: def __init__(self): self._prototipos = {} self._cargar_prototipos()
def _cargar_prototipos(self): self._prototipos["contrato"] = Contrato( titulo="Contrato de Confidencialidad Estándar", contenido="Texto legal base que es costoso de cargar...", firma="Firma Digital de la Empresa" ) self._prototipos["informe"] = Informe( titulo="Plantilla de Informe Mensual", contenido="Estructura base del informe...", firma="Firma del Departamento de BI" )
def crear_documento(self, tipo: str) -> Documento | None: prototipo = self._prototipos.get(tipo) return prototipo.clonar() if prototipo else None
# Instanciamos el registro para que sea un Singleton de factoregistry_instance = DocumentoRegistry()Paso 4: Crear el Cliente (La Vista de Django) 🌐
documentos/views.py
from django.http import JsonResponsefrom .registry import registry_instanceimport dataclasses
def crear_documento(request, tipo: str): autor = request.GET.get("autor", "Desconocido")
# Usamos el registro para obtener un clon nuevo_documento = registry_instance.crear_documento(tipo)
if not nuevo_documento: return JsonResponse({"error": "Tipo de documento no válido"}, status=400)
# Modificamos el clon nuevo_documento.titulo += f" para {autor}" nuevo_documento.firma = autor
# Convertimos el dataclass a dict para la respuesta JSON return JsonResponse(dataclasses.asdict(nuevo_documento))Paso 5: Configurar las URLs y Probar ✅
- Crea
documentos/urls.py:from django.urls import pathfrom . import viewsurlpatterns = [path('crear/<str:tipo>/', views.crear_documento),] - Incluye estas URLs en
prototype_django/urls.py:from django.urls import path, includeurlpatterns = [path('api/documentos/', include('documentos.urls'))] - Ejecuta
python manage.py runservery prueba los endpoints:http://127.0.0.1:8000/api/documentos/crear/contrato/?autor=AngelRespuesta:
{"titulo": "Contrato de Confidencialencialidad Estándar para Angel", "contenido": "Texto legal base...", "firma": "Angel"}http://127.0.0.1:8000/api/documentos/crear/informe/?autor=JesusRespuesta:
{"titulo": "Plantilla de Informe Mensual para Jesus", "contenido": "Estructura base del informe...", "firma": "Jesus"}