Introducción al Patrón Prototype
El patrón de diseño Prototype permite crear nuevos objetos copiando una instancia existente, conocida como prototipo. Este enfoque es particularmente útil cuando la creación directa de un objeto mediante el operador de instanciación es costosa o compleja. Al trabajar con estructuras de datos que gessionan memoria dinámica, como vectores matemáticos o arreglos expansibles, es crucial comprender la diferencia entre copias superficiales y profundas para evitar fugas de memoria o referencias compartidas no deseadas.
Implementación en Java: Clonación Profunda mediante Serializcaión
En Java, una forma robusta de realizar una clonación profunda de objetos que contienen referencias a otros objetos mutables (como listas o arreglos) es utilizar la serialización. A continuación, se presenta una implementación de un vector dinámico que utiliza flujos de bytes para garantizar que el estado interno se duplique de manera independiente.
import java.io.*;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class NumericVector implements Cloneable, Serializable {
private int currentCapacity;
private static final int DEFAULT_CAPACITY = 10;
private List<Double> elements;
public NumericVector() {
this.currentCapacity = DEFAULT_CAPACITY;
this.elements = new ArrayList<>(currentCapacity);
}
public NumericVector(int initialCapacity, Double... items) {
this.currentCapacity = initialCapacity;
this.elements = new ArrayList<>(Arrays.asList(items));
}
public int getCurrentCapacity() {
return currentCapacity;
}
public List<Double> getElements() {
return elements;
}
public void addElement(Double item) {
this.elements.add(item);
}
public void displayElements() {
elements.forEach(e -> System.out.print(e + " "));
System.out.println();
}
public NumericVector performDeepClone() {
try (ByteArrayOutputStream byteOutStream = new ByteArrayOutputStream();
ObjectOutputStream objOutStream = new ObjectOutputStream(byteOutStream)) {
objOutStream.writeObject(this);
try (ByteArrayInputStream byteInStream = new ByteArrayInputStream(byteOutStream.toByteArray());
ObjectInputStream objInStream = new ObjectInputStream(byteInStream)) {
return (NumericVector) objInStream.readObject();
}
} catch (IOException | ClassNotFoundException ex) {
throw new RuntimeException("Error durante la clonación profunda", ex);
}
}
}
Para verificar que la clonación profunda funciona correctamente y que las referencias internas no se comparten, podemos ejecutar el siguiente cliente de prueba:
public class PrototypeClient {
public static void main(String[] args) {
NumericVector originalVector = new NumericVector(5, 1.1, 2.2, 3.3, 4.4, 5.5);
NumericVector clonedVector = originalVector.performDeepClone();
System.out.println("Hash de la lista original: " + originalVector.getElements().hashCode());
System.out.println("Hash de la lista clonada: " + clonedVector.getElements().hashCode());
originalVector.displayElements();
clonedVector.displayElements();
}
}
Implementación en C++: Gestión de Memoria y Clonación
En C++, la gestión de memoria manual requiere un cuidado especial. Al clonar un objeto que contiene punteros a memoria asignada dinámicamente, debemos asegurarnos de asignar nueva memoria y copiar los valores, evitando así que dos objetos apunten a la misma dirección de memoria. El siguiente ejemplo muestra una clase de vector matemático con reasignación dinámica y un método de clonación.
#include <iostream>
#include <cstdlib>
#include <cstring>
class MathVector {
private:
double* dataBuffer;
int currentSize;
int allocatedCapacity;
void allocateMemory(int capacity) {
dataBuffer = new double[capacity];
allocatedCapacity = capacity;
}
public:
MathVector() : currentSize(0), allocatedCapacity(10) {
allocateMemory(allocatedCapacity);
}
MathVector(int size) : currentSize(size), allocatedCapacity(size * 2) {
allocateMemory(allocatedCapacity);
for (int i = 0; i < currentSize; ++i) {
dataBuffer[i] = static_cast<double>(i);
}
}
// Constructor de copia (Clonación profunda manual)
MathVector(const MathVector& other) {
currentSize = other.currentSize;
allocatedCapacity = other.allocatedCapacity;
allocateMemory(allocatedCapacity);
std::memcpy(dataBuffer, other.dataBuffer, currentSize * sizeof(double));
}
~MathVector() {
delete[] dataBuffer;
}
void appendElement(double val) {
if (currentSize >= allocatedCapacity) {
allocatedCapacity *= 2;
double* newBuffer = new double[allocatedCapacity];
std::memcpy(newBuffer, dataBuffer, currentSize * sizeof(double));
delete[] dataBuffer;
dataBuffer = newBuffer;
}
dataBuffer[currentSize++] = val;
}
int getSize() const {
return currentSize;
}
MathVector* cloneObject() const {
return new MathVector(*this);
}
void printData() const {
for (int i = 0; i < currentSize; ++i) {
std::cout << dataBuffer[i] << "\t";
}
std::cout << std::endl;
}
};
int main() {
MathVector* vecA = new MathVector(4);
vecA->appendElement(99.9);
std::cout << "Dirección de memoria de vecA: " << vecA << std::endl;
vecA->printData();
MathVector* vecB = vecA->cloneObject();
std::cout << "Dirección de memoria de vecB: " << vecB << std::endl;
vecB->printData();
delete vecA;
delete vecB;
return 0;
}
Diferencias entre Copia Superficial y Copia Profunda
Cuando se duplica un objeto que contiene referencias o punteros, el comportamiento de la copia puede variar drásticamente:
- Copia Superficial (Shallow Copy): Simplemente replica los valores de los campos del objeto original. Si un campo es un puntero o una referencia, la copia apuntará exactamente a la misma dirección de memoria que el original. Modificar el contenido referenciado afectará a ambos objetos.
- Copia Profunda (Deep Copy): Además de copiar los campos primitivos, crea nuevas instancias para los objetos o bloques de memoria referenciados. Como resultado, el objeto original y el clonado son completamente independientes; cualquier mutación en uno no tendrá impacto en el otro.