Have you ever found yourself stuck in a loop, creating objects that are almost identical but require complex, multi-step initialization? You write long if/else blocks or complex constructors, and every time a new variation is needed, the logic grows more tangled. This approach is not only tedious but also brittle and hard to maintain.
What if you could create a “master copy” of an object and then simply clone it whenever you need a new instance?
This is the core idea behind the Prototype design pattern. It allows you to create new objects by copying a pre-configured instance, known as a prototype, rather than creating them from scratch.
The Problem: Repetitive and Inflexible Object Creation
Imagine a system that generates invoices. You might have several types of invoices: one for monthly subscriptions, one for hosting services, and another for custom development work. While they share common elements like a header, footer, and tax rate, they differ in their line items and descriptions.
A naive implementation might look like this: a single function with a large conditional block that builds the invoice based on its type.
# The "Before" Scenario: A rigid and repetitive function
def create_invoice(customer_type, customer_name, invoice_number):
if customer_type == "subscription":
# Repetitive initialization
invoice = {
"header": "Default Company Header",
"footer": "Default Company Footer",
"tax_rate": 0.15,
"items": [{"description": "Monthly Subscription", "price": 1000}],
"customer_name": customer_name,
"invoice_number": invoice_number,
}
return invoice
elif customer_type == "hosting":
# More repetition
invoice = {
"header": "Default Company Header",
"footer": "Default Company Footer",
"tax_rate": 0.15,
"items": [{"description": "Hosting Service", "price": 1500}],
"customer_name": customer_name,
"invoice_number": invoice_number,
}
return invoice
elif customer_type == "custom_development":
# And again...
invoice = {
"header": "Default Company Header",
"footer": "Default Company Footer",
"tax_rate": 0.18, # A different tax rate complicates things
"items": [{"description": "Custom Development", "price": 2000}],
"customer_name": customer_name,
"invoice_number": invoice_number,
}
return invoice
# ... what if we add more types?
This approach has several major flaws:
- High Coupling: The client code is tightly coupled to the concrete implementation of every invoice type.
- Violation of DRY (Don’t Repeat Yourself): The
header,footer, and other common attributes are repeated in every block. - Maintenance Nightmare: If you need to change the default header, you have to update it in every single
ifblock across the entire application.
The Solution: The Prototype Pattern
The Prototype pattern solves this by establishing a set of “template” objects. When you need a new object, you simply ask a template to clone itself, and then you modify the few unique details on the clone.
Here’s a visual breakdown of the concept:
graph TD
subgraph Prototype Pattern
A[Prototype Instance <br/> (e.g., Subscription Invoice Template)] -->|clones| B(New Object 1);
A -->|clones| C(New Object 2);
A -->|clones| D(New Object 3);
end
subgraph Customization
B --> B1{Set customer='Client A', number=101};
C --> C1{Set customer='Client B', number=102};
D --> D1{Set customer='Client C', number=103};
end
Step-by-Step Implementation in Python
Let’s refactor our invoicing system using the Prototype pattern. We’ll need a prototype “interface,” concrete classes, and a registry to manage our prototypes.
Step 1: Define the Project Structure
A clean structure helps separate concerns. Our prototype, registry, and client code will live in different files.
invoicing_system/
├── prototypes.py # Contains the Prototype ABC and concrete Invoice classes
├── registry.py # Manages our pre-configured invoice templates
└── main.py # The client code that uses the pattern
Step 2: Create the Prototype “Interface”
In Python, we use Abstract Base Classes (ABC) to define interfaces. Our Prototype will have a single abstract method: clone().
File: prototypes.py
import copy
from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from typing import List, Any
@dataclass
class InvoiceItem:
"""A single line item in an invoice."""
description: str
price: float
class Prototype(ABC):
"""The base Prototype class defines the cloning interface."""
@abstractmethod
def clone(self) -> Any:
"""Creates a copy of the object."""
pass
@dataclass
class Invoice(Prototype):
"""The Concrete Prototype: An invoice that can be cloned."""
customer_name: str
invoice_number: int
header: str
footer: str
tax_rate: float
items: List[InvoiceItem] = field(default_factory=list)
def clone(self) -> 'Invoice':
"""
Creates a deep copy of the invoice.
This is crucial for objects with nested mutable structures like lists.
"""
return copy.deepcopy(self)
def calculate_total(self) -> float:
"""Calculates the total cost of the invoice."""
subtotal = sum(item.price for item in self.items)
return subtotal * (1 + self.tax_rate)
[!TIP] Why
copy.deepcopy? TheInvoiceclass contains a list ofInvoiceItemobjects. A simple shallow copy (copy.copy()) would create a newInvoicebut share the same list of items. Modifying the items in a cloned invoice would accidentally change the original template!copy.deepcopyrecursively copies all objects, ensuring the clone is completely independent.
Step 3: Create a Prototype Registry
The registry is a central place to store and retrieve our pre-configured invoice templates. A simple dictionary is perfect for this.
File: registry.py
from prototypes import Invoice, InvoiceItem
class InvoiceRegistry:
"""A registry to store and manage invoice prototypes."""
def __init__(self):
self._prototypes = {}
def register(self, name: str, prototype: Invoice):
"""Register a new prototype."""
self._prototypes[name] = prototype
def get(self, name: str) -> Invoice:
"""Get a prototype by name."""
prototype = self._prototypes.get(name)
if not prototype:
raise ValueError(f"Prototype with name '{name}' not found.")
return prototype.clone()
# --- Pre-configure and register our templates ---
DEFAULT_HEADER = "10xdev Blog - Official Invoice"
DEFAULT_FOOTER = "Thank you for your business!"
# Create the registry instance
invoice_registry = InvoiceRegistry()
# Subscription Template
subscription_prototype = Invoice(
customer_name="",
invoice_number=0,
header=DEFAULT_HEADER,
footer=DEFAULT_FOOTER,
tax_rate=0.15,
items=[InvoiceItem(description="Monthly Pro Subscription", price=100.0)]
)
invoice_registry.register("subscription", subscription_prototype)
# Hosting Template
hosting_prototype = Invoice(
customer_name="",
invoice_number=0,
header=DEFAULT_HEADER,
footer=DEFAULT_FOOTER,
tax_rate=0.15,
items=[InvoiceItem(description="Cloud Hosting Service (1 Year)", price=1500.0)]
)
invoice_registry.register("hosting", hosting_prototype)
# Custom Development Template
custom_dev_prototype = Invoice(
customer_name="",
invoice_number=0,
header=DEFAULT_HEADER,
footer=DEFAULT_FOOTER,
tax_rate=0.18, # Note the different tax rate
items=[InvoiceItem(description="Custom Feature Development", price=2000.0)]
)
invoice_registry.register("custom_development", custom_dev_prototype)
Step 4: The Refactored Client Code
Now, the client code becomes incredibly simple and clean. It just asks the registry for a template, clones it, and fills in the unique details.
File: main.py
from registry import invoice_registry
import datetime
def generate_customer_invoices(customers):
"""
Generates invoices for a list of customers using the Prototype pattern.
"""
generated_invoices = []
for i, customer in enumerate(customers):
# 1. Get the correct prototype from the registry and clone it
new_invoice = invoice_registry.get(customer["type"])
# 2. Customize the clone with dynamic data
new_invoice.customer_name = customer["name"]
new_invoice.invoice_number = 1000 + i
# You could add more dynamic items if needed
# new_invoice.items.append(InvoiceItem("Processing Fee", 5.0))
generated_invoices.append(new_invoice)
print(f"--- Generated Invoice #{new_invoice.invoice_number} ---")
print(f" Customer: {new_invoice.customer_name}")
print(f" Header: {new_invoice.header}")
for item in new_invoice.items:
print(f" - {item.description}: ${item.price:.2f}")
print(f" Tax Rate: {new_invoice.tax_rate:.2%}")
print(f" Total: ${new_invoice.calculate_total():.2f}")
print(f" Footer: {new_invoice.footer}\n")
# --- Example Usage ---
customers_from_db = [
{"name": "Client A", "type": "subscription"},
{"name": "Client B", "type": "hosting"},
{"name": "Client C", "type": "custom_development"},
]
generate_customer_invoices(customers_from_db)
Visualizing the Code Evolution
Using the Prototype pattern dramatically cleaned up our create_invoice logic. Here’s a “diff” view of the transformation:
- # The old, messy way
- def create_invoice(customer_type, customer_name, invoice_number):
- if customer_type == "subscription":
- invoice = {
- "header": "Default Company Header",
- "footer": "Default Company Footer",
- "tax_rate": 0.15,
- "items": [{"description": "Monthly Subscription", "price": 1000}],
- "customer_name": customer_name,
- "invoice_number": invoice_number,
- }
- return invoice
- elif customer_type == "hosting":
- # ... more repetitive code
- pass
+ # The new, clean way with the Prototype pattern
+ def generate_customer_invoices(customers):
+ for i, customer in enumerate(customers):
+ # 1. Get a clone of the correct prototype
+ new_invoice = invoice_registry.get(customer["type"])
+
+ # 2. Customize the unique details
+ new_invoice.customer_name = customer["name"]
+ new_invoice.invoice_number = 1000 + i
+
+ # Ready to use!
+ print(f"Generated invoice for {new_invoice.customer_name}")
Class and Sequence Diagrams
To better understand the relationships and interactions, let’s look at the diagrams.
Class Diagram
This diagram shows how our classes are structured. Invoice implements the Prototype interface and the InvoiceRegistry holds instances of Invoice.
classDiagram
class Prototype {
<<ABC>>
+clone()* Any
}
class Invoice {
-customer_name: str
-invoice_number: int
-header: str
-footer: str
-tax_rate: float
-items: List~InvoiceItem~
+clone() Invoice
+calculate_total() float
}
class InvoiceItem {
-description: str
-price: float
}
class InvoiceRegistry {
-prototypes: dict
+register(name, prototype)
+get(name) Invoice
}
Prototype <|-- Invoice
Invoice "1" *-- "many" InvoiceItem
InvoiceRegistry ..> Invoice : uses
Sequence Diagram
This diagram illustrates the runtime interaction: the client asks the registry for an object, the registry clones its prototype, and returns the new copy.
sequenceDiagram
participant Client as main.py
participant Registry as InvoiceRegistry
participant Prototype as subscription_prototype
participant NewInvoice as new_invoice (Clone)
Client->>Registry: get("subscription")
activate Registry
Registry->>Prototype: clone()
activate Prototype
Prototype-->>Registry: returns deep copy
deactivate Prototype
Registry-->>Client: returns new_invoice
deactivate Registry
Client->>NewInvoice: Set customer_name
Client->>NewInvoice: Set invoice_number
When Should You Use the Prototype Pattern?
The Prototype pattern is most effective in the following scenarios:
- When object creation is expensive: If creating an object involves costly operations (e.g., database calls, network requests, heavy computation), you can create it once, cache it, and then clone it for subsequent uses.
- When your code shouldn’t depend on concrete classes: The client code can work with any object that conforms to the
Prototypeinterface, without needing to know its specific class. - When you have many object variations that only differ slightly: As seen in our invoice example, it’s much cleaner to have a few templates than to manage countless subclasses or complex constructors.
[!WARNING] Beware of Circular References
copy.deepcopyis powerful, but it can get stuck in an infinite loop if objects reference each other (e.g.,A.child = BandB.parent = A). While modern Python handles this, it’s a potential edge case to be aware of in complex object graphs.
Conclusion
The Prototype pattern offers a powerful and elegant solution to the problem of complex object creation. By shifting from a “creation from scratch” mindset to a “clone and customize” approach, you can significantly reduce code duplication, decouple your system, and make your application more flexible and maintainable. The next time you find yourself writing a long chain of if/elif/else to build objects, consider if a prototype could be your blueprint for cleaner code.