Design Pattern 9: Prototype Pattern - Efficient Object Cloning for Resource Management and Performance Optimization

Download the complete Design Pattern series code from the design_pattern repo.

Introduction: The Power of Object Cloning

The Prototype Pattern is a creational design pattern that allows you to create new objects by cloning existing ones, avoiding the overhead of creating objects from scratch. This pattern is particularly useful when object creation is expensive or when you need to create objects with similar initial states.

Real-World Applications

The Prototype Pattern is widely used in:

  • Graphics Applications: Cloning shapes, sprites, and UI elements
  • Game Development: Creating multiple instances of game objects
  • Document Editors: Copying and pasting complex objects
  • Configuration Management: Creating variations of configuration objects
  • Database Operations: Cloning data transfer objects (DTOs)

Case Study: Music Light Show App

This pattern reminds me of a music light show editing app I developed. The app allows users to create complex light sequences, and users often need to copy and paste light patterns to save time.

Problem Statement: Copy & Paste Functionality

Users requested a copy & paste feature to avoid recreating similar light patterns from scratch:

Object-Oriented Analysis (OOA)

Let’s analyze the requirements and design our initial solution:

When we need to copy LightShowData, we can simply create a new instance with the same JSON data.

Identifying Design Forces

Without the Prototype Pattern, we encounter several challenges:

  1. Complex Constructors: If constructors have many parameters, creating new instances requires detailed knowledge
  2. Performance Overhead: If object creation involves expensive computations, recreating objects becomes inefficient
  3. State Management: Ensuring copied objects have the same state as originals can be complex
  4. Memory Usage: Creating objects from scratch may involve unnecessary resource allocation

Applying Prototype Pattern Solution

The Prototype Pattern provides an elegant solution by allowing objects to clone themselves.

Prototype Pattern UML Structure

Key Components:

  • Prototype: Abstract interface defining the clone method
  • Concrete Prototype: Implements the clone method to create exact copies

Applied to Light Show App

Implementation: Object-Oriented Programming (OOP)

Prototype Interface

interface LightShowDataPrototype {
    val startIndex: Int
    val lightDataList: List<Int>
    fun clone(): LightShowDataPrototype
}

Concrete Prototype Implementation

class LightShowData: LightShowDataPrototype {
    override val startIndex: Int
    override val lightDataList: List<Int>

    constructor(originalDataList: List<Int>) {
        startIndex = originalDataList[0]
        lightDataList = originalDataList.subList(1, originalDataList.size).map { it * 2 }
    }

    constructor(startIndex: Int, lightDataList: List<Int>) {
        this.startIndex = startIndex
        this.lightDataList = lightDataList
    }

    override fun clone(): LightShowDataPrototype {
        return LightShowData(startIndex, lightDataList.toList())
    }
}

Client Usage

fun main() {
    val originalData = listOf(1, 2, 3, 4, 5)

    // Before using prototype pattern - expensive recreation
    val originalLightShowData: LightShowDataPrototype = LightShowData(originalData)
    val newLightShowData: LightShowDataPrototype = LightShowData(originalData)

    println("Original: $originalLightShowData")
    println("New (expensive): $newLightShowData")

    // After using prototype pattern - efficient cloning
    val clonedLightShowData: LightShowDataPrototype = originalLightShowData.clone()

    println("Original: $originalLightShowData")
    println("Cloned (efficient): $clonedLightShowData")
}

By using the clone() method, we avoid repeating the expensive computation:

originalDataList.subList(1, originalDataList.size).map { it * 2 }

Advanced Implementation: Deep vs Shallow Copy

Shallow Copy Implementation

class UserProfile: Cloneable {
    var name: String = ""
    var email: String = ""
    var preferences: MutableList<String> = mutableListOf()
    
    public override fun clone(): UserProfile {
        return super.clone() as UserProfile
    }
}

Deep Copy Implementation

class UserProfile: Cloneable {
    var name: String = ""
    var email: String = ""
    var preferences: MutableList<String> = mutableListOf()
    
    public override fun clone(): UserProfile {
        val cloned = super.clone() as UserProfile
        cloned.preferences = preferences.toMutableList() // Deep copy of list
        return cloned
    }
}

Real-World Example: Graphics System

abstract class Shape: Cloneable {
    var color: String = "black"
    var x: Int = 0
    var y: Int = 0
    
    abstract fun draw()
    
    public override fun clone(): Shape {
        return super.clone() as Shape
    }
}

class Circle: Shape() {
    var radius: Int = 10
    
    override fun draw() {
        println("Drawing circle at ($x, $y) with radius $radius and color $color")
    }
    
    override fun clone(): Circle {
        val cloned = super.clone() as Circle
        cloned.radius = this.radius
        return cloned
    }
}

class Rectangle: Shape() {
    var width: Int = 20
    var height: Int = 15
    
    override fun draw() {
        println("Drawing rectangle at ($x, $y) with size ${width}x${height} and color $color")
    }
    
    override fun clone(): Rectangle {
        val cloned = super.clone() as Rectangle
        cloned.width = this.width
        cloned.height = this.height
        return cloned
    }
}

Best Practices and Considerations

1. Clone Method Implementation

// Good: Proper clone implementation
class ComplexObject: Cloneable {
    private var data: MutableList<String> = mutableListOf()
    private var metadata: Map<String, Any> = mapOf()
    
    public override fun clone(): ComplexObject {
        val cloned = super.clone() as ComplexObject
        cloned.data = data.toMutableList() // Deep copy
        cloned.metadata = metadata.toMap() // Deep copy
        return cloned
    }
}

// Avoid: Incomplete cloning
class BadClone: Cloneable {
    private var data: MutableList<String> = mutableListOf()
    
    public override fun clone(): BadClone {
        return super.clone() as BadClone // Shallow copy of mutable list!
    }
}

2. Prototype Registry

class PrototypeRegistry {
    private val prototypes = mutableMapOf<String, Cloneable>()
    
    fun addPrototype(name: String, prototype: Cloneable) {
        prototypes[name] = prototype
    }
    
    fun getClone(name: String): Cloneable? {
        return prototypes[name]?.let { it.clone() }
    }
}

// Usage
val registry = PrototypeRegistry()
registry.addPrototype("default-user", UserProfile())
val newUser = registry.getClone("default-user") as UserProfile

3. Performance Optimization

class ExpensiveObject: Cloneable {
    private var computedValue: String? = null
    
    private fun computeExpensiveValue(): String {
        // Expensive computation
        return "computed result"
    }
    
    fun getValue(): String {
        if (computedValue == null) {
            computedValue = computeExpensiveValue()
        }
        return computedValue!!
    }
    
    public override fun clone(): ExpensiveObject {
        val cloned = super.clone() as ExpensiveObject
        cloned.computedValue = this.computedValue // Preserve computed value
        return cloned
    }
}

Performance Comparison

Approach Memory Usage Performance Complexity
New Constructor High Low High
Prototype Clone Low High Low
Factory Method Medium Medium Medium
  • Factory Method: Creates objects without specifying exact classes
  • Abstract Factory: Creates families of related objects
  • Builder: Constructs complex objects step by step
  • Singleton: Ensures only one instance exists

Conclusion

The Prototype Pattern provides an efficient way to create object copies while avoiding the overhead of object creation from scratch. Key benefits include:

  • Performance Improvement: Avoids expensive object creation processes
  • Simplified Object Creation: Reduces complexity of object instantiation
  • State Preservation: Ensures copied objects maintain the same state
  • Memory Efficiency: Reduces resource allocation overhead

This pattern is particularly valuable in scenarios where object creation is expensive or when you need to create multiple similar objects efficiently.




    Enjoy Reading This Article?

    Here are some more articles you might like to read next:

  • How to Use Multiple GitHub Accounts on One Computer: Complete SSH Setup Guide
  • Excalidraw AI: Create Professional Diagrams with Text Commands - Complete Guide
  • Complete macOS Development Environment Setup Guide for 2024
  • Design Pattern 28: Interpreter Pattern - Complete Guide with Examples
  • Design Pattern 27: Visitor Pattern - Complete Guide with Real-World IoT Examples