The Prototype Pattern is a creational design pattern that allows you to create new objects by copying existing objects (prototypes) without depending on their classes. This pattern is particularly useful when object creation is expensive or complex.
Key Concepts
- Prototype Interface: Declares the cloning method
- Concrete Prototype: Implements the cloning method
- Client: Creates new objects by copying prototypes
Basic Implementation
Example 1: Basic Prototype Pattern
// Prototype interface interface Prototype extends Cloneable { Prototype clone() throws CloneNotSupportedException; void display(); } // Concrete prototype class ConcretePrototype implements Prototype { private String name; private int value; public ConcretePrototype(String name, int value) { this.name = name; this.value = value; System.out.println("Original object created with complex initialization..."); simulateExpensiveOperation(); } private void simulateExpensiveOperation() { try { Thread.sleep(1000); // Simulate expensive operation } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } // Copy constructor for deep cloning public ConcretePrototype(ConcretePrototype prototype) { this.name = prototype.name; this.value = prototype.value; } @Override public Prototype clone() throws CloneNotSupportedException { // Shallow clone return (ConcretePrototype) super.clone(); } public Prototype deepClone() { // Deep clone using copy constructor return new ConcretePrototype(this); } @Override public void display() { System.out.println("Prototype [name=" + name + ", value=" + value + ", hashCode=" + hashCode() + "]"); } // Getters and setters public String getName() { return name; } public void setName(String name) { this.name = name; } public int getValue() { return value; } public void setValue(int value) { this.value = value; } } // Client code public class PrototypeBasicDemo { public static void main(String[] args) throws CloneNotSupportedException { // Create original object (expensive operation) ConcretePrototype original = new ConcretePrototype("Original", 100); original.display(); // Create clone (cheap operation) ConcretePrototype clone = (ConcretePrototype) original.clone(); clone.setName("Clone"); clone.display(); // Create deep clone ConcretePrototype deepClone = (ConcretePrototype) original.deepClone(); deepClone.setName("Deep Clone"); deepClone.display(); } } Advanced Prototype Pattern with Registry
Example 2: Prototype Registry
import java.util.HashMap; import java.util.Map; // Prototype registry class PrototypeRegistry { private Map<String, Prototype> prototypes = new HashMap<>(); public void registerPrototype(String key, Prototype prototype) { prototypes.put(key, prototype); } public Prototype getPrototype(String key) throws CloneNotSupportedException { Prototype prototype = prototypes.get(key); if (prototype != null) { return prototype.clone(); } throw new IllegalArgumentException("Prototype not found for key: " + key); } public void loadDefaults() { registerPrototype("basic", new ConcretePrototype("Basic", 1)); registerPrototype("advanced", new ConcretePrototype("Advanced", 2)); registerPrototype("premium", new ConcretePrototype("Premium", 3)); } } // Enhanced prototype with different types class DocumentPrototype implements Prototype { private String title; private String content; private String author; private DocumentType type; private Map<String, String> metadata; public DocumentPrototype(String title, String content, String author, DocumentType type) { this.title = title; this.content = content; this.author = author; this.type = type; this.metadata = new HashMap<>(); simulateDocumentLoading(); } // Copy constructor public DocumentPrototype(DocumentPrototype prototype) { this.title = prototype.title; this.content = prototype.content; this.author = prototype.author; this.type = prototype.type; this.metadata = new HashMap<>(prototype.metadata); // Deep copy } private void simulateDocumentLoading() { System.out.println("Loading document: " + title); try { Thread.sleep(500); // Simulate document loading time } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } @Override public Prototype clone() throws CloneNotSupportedException { // Shallow clone - be careful with mutable objects! return (DocumentPrototype) super.clone(); } public Prototype deepClone() { return new DocumentPrototype(this); } @Override public void display() { System.out.println("Document [title=" + title + ", type=" + type + ", author=" + author + ", contentLength=" + content.length() + ", metadata=" + metadata + "]"); } // Business methods public void addMetadata(String key, String value) { metadata.put(key, value); } public void updateContent(String newContent) { this.content = newContent; } // Getters and setters public String getTitle() { return title; } public void setTitle(String title) { this.title = title; } public String getContent() { return content; } public String getAuthor() { return author; } public void setAuthor(String author) { this.author = author; } public DocumentType getType() { return type; } } enum DocumentType { REPORT, CONTRACT, PRESENTATION, EMAIL } // Usage example public class PrototypeRegistryDemo { public static void main(String[] args) throws CloneNotSupportedException { PrototypeRegistry registry = new PrototypeRegistry(); registry.loadDefaults(); // Create documents from prototypes DocumentPrototype reportTemplate = new DocumentPrototype( "Monthly Report", "This is a template for monthly reports.", "System", DocumentType.REPORT ); reportTemplate.addMetadata("category", "finance"); reportTemplate.addMetadata("version", "1.0"); registry.registerPrototype("monthly-report", reportTemplate); // Clone prototypes to create new instances DocumentPrototype janReport = (DocumentPrototype) registry.getPrototype("monthly-report"); janReport.setTitle("January 2024 Monthly Report"); janReport.setAuthor("John Doe"); janReport.updateContent("January was a great month for sales..."); janReport.addMetadata("period", "January 2024"); janReport.display(); DocumentPrototype febReport = (DocumentPrototype) registry.getPrototype("monthly-report"); febReport.setTitle("February 2024 Monthly Report"); febReport.setAuthor("Jane Smith"); febReport.updateContent("February showed steady growth..."); febReport.addMetadata("period", "February 2024"); febReport.display(); } } Real-World Example: Graphic Shapes
Example 3: Graphic Editor with Prototype Pattern
import java.awt.*; import java.util.ArrayList; import java.util.List; // Abstract shape prototype abstract class Shape implements Cloneable { protected int x; protected int y; protected Color color; protected boolean selected; public Shape(int x, int y, Color color) { this.x = x; this.y = y; this.color = color; this.selected = false; } // Copy constructor for deep cloning public Shape(Shape shape) { this.x = shape.x; this.y = shape.y; this.color = shape.color; this.selected = shape.selected; } @Override public Shape clone() throws CloneNotSupportedException { return (Shape) super.clone(); } public abstract Shape deepClone(); public abstract void draw(Graphics g); public abstract boolean contains(int x, int y); // Common operations public void move(int dx, int dy) { this.x += dx; this.y += dy; } public void setSelected(boolean selected) { this.selected = selected; } // Getters and setters public int getX() { return x; } public void setX(int x) { this.x = x; } public int getY() { return y; } public void setY(int y) { this.y = y; } public Color getColor() { return color; } public void setColor(Color color) { this.color = color; } } // Concrete shape: Circle class Circle extends Shape { private int radius; public Circle(int x, int y, int radius, Color color) { super(x, y, color); this.radius = radius; } // Copy constructor public Circle(Circle circle) { super(circle); this.radius = circle.radius; } @Override public Shape deepClone() { return new Circle(this); } @Override public void draw(Graphics g) { g.setColor(color); g.fillOval(x - radius, y - radius, radius * 2, radius * 2); if (selected) { g.setColor(Color.RED); g.drawOval(x - radius, y - radius, radius * 2, radius * 2); } } @Override public boolean contains(int pointX, int pointY) { int dx = pointX - x; int dy = pointY - y; return dx * dx + dy * dy <= radius * radius; } public int getRadius() { return radius; } public void setRadius(int radius) { this.radius = radius; } } // Concrete shape: Rectangle class Rectangle extends Shape { private int width; private int height; public Rectangle(int x, int y, int width, int height, Color color) { super(x, y, color); this.width = width; this.height = height; } // Copy constructor public Rectangle(Rectangle rectangle) { super(rectangle); this.width = rectangle.width; this.height = rectangle.height; } @Override public Shape deepClone() { return new Rectangle(this); } @Override public void draw(Graphics g) { g.setColor(color); g.fillRect(x, y, width, height); if (selected) { g.setColor(Color.RED); g.drawRect(x, y, width, height); } } @Override public boolean contains(int pointX, int pointY) { return pointX >= x && pointX <= x + width && pointY >= y && pointY <= y + height; } public int getWidth() { return width; } public void setWidth(int width) { this.width = width; } public int getHeight() { return height; } public void setHeight(int height) { this.height = height; } } // Graphic editor using prototype pattern class GraphicEditor { private List<Shape> shapes = new ArrayList<>(); private Shape selectedShape; public void addShape(Shape shape) { shapes.add(shape); } public void selectShape(int x, int y) { // Deselect all shapes shapes.forEach(shape -> shape.setSelected(false)); selectedShape = null; // Find and select clicked shape for (int i = shapes.size() - 1; i >= 0; i--) { Shape shape = shapes.get(i); if (shape.contains(x, y)) { shape.setSelected(true); selectedShape = shape; break; } } } public void duplicateSelectedShape() { if (selectedShape != null) { try { Shape clone = selectedShape.deepClone(); clone.move(20, 20); // Offset the clone shapes.add(clone); selectedShape.setSelected(false); clone.setSelected(true); selectedShape = clone; } catch (Exception e) { e.printStackTrace(); } } } public void drawAll(Graphics g) { shapes.forEach(shape -> shape.draw(g)); } public List<Shape> getShapes() { return shapes; } } // Demo application public class GraphicEditorDemo { public static void main(String[] args) { // Create prototype shapes Circle circlePrototype = new Circle(100, 100, 30, Color.BLUE); Rectangle rectanglePrototype = new Rectangle(200, 100, 80, 60, Color.GREEN); GraphicEditor editor = new GraphicEditor(); // Add original shapes editor.addShape(circlePrototype); editor.addShape(rectanglePrototype); // Select and duplicate shapes editor.selectShape(100, 100); // Select circle editor.duplicateSelectedShape(); // Duplicate circle editor.selectShape(200, 100); // Select rectangle editor.duplicateSelectedShape(); // Duplicate rectangle // Display results System.out.println("Total shapes: " + editor.getShapes().size()); editor.getShapes().forEach(shape -> { System.out.println(shape.getClass().getSimpleName() + " at (" + shape.getX() + ", " + shape.getY() + ")"); }); } } Database Entity Prototypes
Example 4: Database Entity Cloning
import java.time.LocalDateTime; import java.util.ArrayList; import java.util.List; import java.util.UUID; // Base entity prototype abstract class Entity implements Cloneable { protected String id; protected LocalDateTime createdAt; protected LocalDateTime updatedAt; protected String createdBy; public Entity() { this.id = UUID.randomUUID().toString(); this.createdAt = LocalDateTime.now(); } // Copy constructor public Entity(Entity entity) { this.id = UUID.randomUUID().toString(); // New ID for clone this.createdAt = LocalDateTime.now(); this.updatedAt = entity.updatedAt; this.createdBy = entity.createdBy; } @Override public Entity clone() throws CloneNotSupportedException { return (Entity) super.clone(); } public abstract Entity deepClone(); // Getters and setters public String getId() { return id; } public LocalDateTime getCreatedAt() { return createdAt; } public LocalDateTime getUpdatedAt() { return updatedAt; } public void setUpdatedAt(LocalDateTime updatedAt) { this.updatedAt = updatedAt; } public String getCreatedBy() { return createdBy; } public void setCreatedBy(String createdBy) { this.createdBy = createdBy; } } // User entity class User extends Entity { private String username; private String email; private List<String> roles; private UserProfile profile; public User(String username, String email) { super(); this.username = username; this.email = email; this.roles = new ArrayList<>(); this.profile = new UserProfile(); } // Copy constructor for deep cloning public User(User user) { super(user); this.username = user.username; this.email = user.email; this.roles = new ArrayList<>(user.roles); // Deep copy this.profile = user.profile.deepClone(); // Deep copy } @Override public User deepClone() { return new User(this); } public void addRole(String role) { roles.add(role); } // Getters and setters public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String getEmail() { return email; } public void setEmail(String email) { this.email = email; } public List<String> getRoles() { return new ArrayList<>(roles); } public void setRoles(List<String> roles) { this.roles = new ArrayList<>(roles); } public UserProfile getProfile() { return profile; } public void setProfile(UserProfile profile) { this.profile = profile; } @Override public String toString() { return "User{id='" + id + "', username='" + username + "', email='" + email + "', roles=" + roles + ", profile=" + profile + "}"; } } // User profile (complex nested object) class UserProfile implements Cloneable { private String firstName; private String lastName; private String phone; private Address address; public UserProfile() { this.address = new Address(); } // Copy constructor public UserProfile(UserProfile profile) { this.firstName = profile.firstName; this.lastName = profile.lastName; this.phone = profile.phone; this.address = profile.address.deepClone(); // Deep copy } public UserProfile deepClone() { return new UserProfile(this); } @Override public UserProfile clone() throws CloneNotSupportedException { UserProfile clone = (UserProfile) super.clone(); clone.address = this.address.clone(); return clone; } // Getters and setters public String getFirstName() { return firstName; } public void setFirstName(String firstName) { this.firstName = firstName; } public String getLastName() { return lastName; } public void setLastName(String lastName) { this.lastName = lastName; } public String getPhone() { return phone; } public void setPhone(String phone) { this.phone = phone; } public Address getAddress() { return address; } public void setAddress(Address address) { this.address = address; } @Override public String toString() { return "UserProfile{firstName='" + firstName + "', lastName='" + lastName + "', phone='" + phone + "', address=" + address + "}"; } } // Address class class Address implements Cloneable { private String street; private String city; private String zipCode; public Address deepClone() { try { return (Address) this.clone(); } catch (CloneNotSupportedException e) { throw new RuntimeException("Cloning not supported", e); } } @Override public Address clone() throws CloneNotSupportedException { return (Address) super.clone(); } // Getters and setters public String getStreet() { return street; } public void setStreet(String street) { this.street = street; } public String getCity() { return city; } public void setCity(String city) { this.city = city; } public String getZipCode() { return zipCode; } public void setZipCode(String zipCode) { this.zipCode = zipCode; } @Override public String toString() { return "Address{street='" + street + "', city='" + city + "', zipCode='" + zipCode + "'}"; } } // Entity factory using prototype pattern class EntityFactory { private User adminUserPrototype; private User guestUserPrototype; public EntityFactory() { // Create prototype instances adminUserPrototype = new User("admin", "[email protected]"); adminUserPrototype.addRole("ADMIN"); adminUserPrototype.addRole("USER"); adminUserPrototype.getProfile().setFirstName("System"); adminUserPrototype.getProfile().setLastName("Administrator"); guestUserPrototype = new User("guest", "[email protected]"); guestUserPrototype.addRole("GUEST"); } public User createAdminUser() { User admin = adminUserPrototype.deepClone(); admin.setCreatedBy("system"); return admin; } public User createGuestUser() { User guest = guestUserPrototype.deepClone(); guest.setCreatedBy("system"); return guest; } public User createUserWithProfile(String username, String email, UserProfile profile) { User user = new User(username, email); user.setProfile(profile.deepClone()); user.addRole("USER"); return user; } } // Demo public class EntityPrototypeDemo { public static void main(String[] args) { EntityFactory factory = new EntityFactory(); // Create users from prototypes User admin1 = factory.createAdminUser(); admin1.setUsername("admin1"); admin1.setEmail("[email protected]"); User admin2 = factory.createAdminUser(); admin2.setUsername("admin2"); admin2.setEmail("[email protected]"); User guest = factory.createGuestUser(); guest.setUsername("temporary_guest"); // Create user with custom profile UserProfile profile = new UserProfile(); profile.setFirstName("John"); profile.setLastName("Doe"); profile.getAddress().setCity("New York"); User customUser = factory.createUserWithProfile("johndoe", "[email protected]", profile); // Display users System.out.println("Admin 1: " + admin1); System.out.println("Admin 2: " + admin2); System.out.println("Guest: " + guest); System.out.println("Custom User: " + customUser); // Verify they have different IDs (different objects) System.out.println("\nDifferent IDs: " + (!admin1.getId().equals(admin2.getId()) && !guest.getId().equals(customUser.getId()))); } } Performance Considerations
Example 5: Prototype vs New Object Creation
public class PerformanceComparison { static class ExpensiveObject implements Cloneable { private byte[] data; public ExpensiveObject() { // Simulate expensive initialization this.data = new byte[1024 * 1024]; // 1MB initializeData(); } // Copy constructor for prototype public ExpensiveObject(ExpensiveObject prototype) { this.data = prototype.data.clone(); // Copy array } private void initializeData() { // Simulate expensive computation for (int i = 0; i < data.length; i++) { data[i] = (byte) (i % 256); } try { Thread.sleep(100); // Simulate computation time } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } @Override public ExpensiveObject clone() throws CloneNotSupportedException { return (ExpensiveObject) super.clone(); // Shallow clone } public ExpensiveObject deepClone() { return new ExpensiveObject(this); } } public static void main(String[] args) throws CloneNotSupportedException { int iterations = 100; // Test creating new objects long startTime = System.currentTimeMillis(); for (int i = 0; i < iterations; i++) { ExpensiveObject obj = new ExpensiveObject(); } long newObjectTime = System.currentTimeMillis() - startTime; // Test cloning from prototype ExpensiveObject prototype = new ExpensiveObject(); startTime = System.currentTimeMillis(); for (int i = 0; i < iterations; i++) { ExpensiveObject clone = prototype.deepClone(); } long cloneTime = System.currentTimeMillis() - startTime; System.out.println("Time to create " + iterations + " new objects: " + newObjectTime + "ms"); System.out.println("Time to create " + iterations + " clones: " + cloneTime + "ms"); System.out.println("Performance improvement: " + ((double) newObjectTime / cloneTime) + "x faster"); } } Best Practices and Considerations
1. Deep vs Shallow Copy
// Shallow copy - be careful with mutable objects! @Override public Object clone() throws CloneNotSupportedException { return super.clone(); } // Deep copy - safer but more expensive public Prototype deepClone() { return new ConcretePrototype(this); } 2. Using Copy Constructors
// Prefer copy constructors over Cloneable public class MyClass { public MyClass(MyClass other) { // Copy all fields, including deep copies of mutable objects } } 3. Registry Pattern
// Maintain a registry of prototypes class PrototypeRegistry { private Map<String, Prototype> registry = new HashMap<>(); public void register(String key, Prototype prototype) { registry.put(key, prototype); } public Prototype create(String key) { return registry.get(key).clone(); } } When to Use Prototype Pattern
- Object creation is expensive (database calls, network operations)
- System should be independent of how objects are created
- Classes to instantiate are specified at runtime
- Avoid building class hierarchy of factories
- Need to avoid constructor complexity
Advantages and Disadvantages
Advantages:
- Reduces subclassing
- Hides complexity of object creation
- Can add/remove objects at runtime
- Improves performance when object creation is expensive
Disadvantages:
- Complex object copying (deep vs shallow)
- Circular references can be problematic
- Requires careful implementation for mutable objects
The Prototype Pattern is particularly useful in scenarios like graphic editors, game development, and enterprise applications where you need to create multiple similar objects with minor variations.