Creational Design Patterns in Python¶
What Are Creational Design Patterns?¶
Creational design patterns abstract the instantiation process, making systems independent of how objects are created, composed, and represented. They help manage complexity when creating objects and make your code more maintainable.
The Five Creational Patterns¶
1. Singleton Pattern¶
The Singleton pattern ensures a class has only one instance and provides a global point of access to it.
Use Case: Database connections, logging, configuration managers
class DatabaseConnection:
_instance = None
def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
cls._instance.connection = None
return cls._instance
def connect(self, connection_string):
if not self.connection:
self.connection = connection_string
print(f"Connected to: {connection_string}")
return self.connection
# Usage
db1 = DatabaseConnection()
db1.connect("postgresql://localhost:5432/mydb")
db2 = DatabaseConnection()
print(db1 is db2) # True - same instance
2. Factory Method Pattern¶
Factory Method defines an interface for creating objects but lets subclasses decide which class to instantiate.
Use Case: Document generators, UI component creation, payment processors
from abc import ABC, abstractmethod
class PaymentProcessor(ABC):
@abstractmethod
def process_payment(self, amount):
pass
class CreditCardProcessor(PaymentProcessor):
def process_payment(self, amount):
return f"Processing ${amount} via Credit Card"
class PayPalProcessor(PaymentProcessor):
def process_payment(self, amount):
return f"Processing ${amount} via PayPal"
class PaymentFactory:
@staticmethod
def create_processor(payment_type):
if payment_type == "credit_card":
return CreditCardProcessor()
elif payment_type == "paypal":
return PayPalProcessor()
else:
raise ValueError(f"Unknown payment type: {payment_type}")
# Usage
processor = PaymentFactory.create_processor("paypal")
print(processor.process_payment(100))
3. Abstract Factory Pattern¶
Abstract Factory provides an interface for creating families of related objects without specifying their concrete classes.
Use Case: Cross-platform UI toolkits, theme systems
from abc import ABC, abstractmethod
# Abstract Products
class Button(ABC):
@abstractmethod
def render(self):
pass
class Checkbox(ABC):
@abstractmethod
def render(self):
pass
# Concrete Products - Windows
class WindowsButton(Button):
def render(self):
return "Rendering Windows Button"
class WindowsCheckbox(Checkbox):
def render(self):
return "Rendering Windows Checkbox"
# Concrete Products - Mac
class MacButton(Button):
def render(self):
return "Rendering Mac Button"
class MacCheckbox(Checkbox):
def render(self):
return "Rendering Mac Checkbox"
# Abstract Factory
class GUIFactory(ABC):
@abstractmethod
def create_button(self) -> Button:
pass
@abstractmethod
def create_checkbox(self) -> Checkbox:
pass
# Concrete Factories
class WindowsFactory(GUIFactory):
def create_button(self):
return WindowsButton()
def create_checkbox(self):
return WindowsCheckbox()
class MacFactory(GUIFactory):
def create_button(self):
return MacButton()
def create_checkbox(self):
return MacCheckbox()
# Usage
def render_ui(factory: GUIFactory):
button = factory.create_button()
checkbox = factory.create_checkbox()
print(button.render())
print(checkbox.render())
render_ui(MacFactory())
4. Builder Pattern¶
Builder separates the construction of complex objects from their representation, allowing the same construction process to create different representations.
Use Case: Building complex objects like HTTP requests, SQL queries, or configuration objects
class Pizza:
def __init__(self):
self.size = None
self.crust = None
self.toppings = []
def __str__(self):
return f"{self.size} pizza with {self.crust} crust and toppings: {', '.join(self.toppings)}"
class PizzaBuilder:
def __init__(self):
self.pizza = Pizza()
def set_size(self, size):
self.pizza.size = size
return self
def set_crust(self, crust):
self.pizza.crust = crust
return self
def add_topping(self, topping):
self.pizza.toppings.append(topping)
return self
def build(self):
return self.pizza
# Usage
pizza = (PizzaBuilder()
.set_size("Large")
.set_crust("Thin")
.add_topping("Pepperoni")
.add_topping("Mushrooms")
.add_topping("Extra Cheese")
.build())
print(pizza)
5. Prototype Pattern¶
Prototype creates new objects by copying existing objects (prototypes) rather than creating them from scratch.
Use Case: Cloning complex objects, avoiding expensive initialization
import copy
class GameCharacter:
def __init__(self, name, level, equipment):
self.name = name
self.level = level
self.equipment = equipment # List of items
def clone(self):
# Deep copy to avoid sharing mutable objects
return copy.deepcopy(self)
def __str__(self):
return f"{self.name} (Level {self.level}) - Equipment: {self.equipment}"
# Usage
original_warrior = GameCharacter("Warrior", 50, ["Sword", "Shield", "Armor"])
print(f"Original: {original_warrior}")
# Clone and modify
cloned_warrior = original_warrior.clone()
cloned_warrior.name = "Shadow Warrior"
cloned_warrior.equipment.append("Magic Ring")
print(f"Clone: {cloned_warrior}")
print(f"Original after cloning: {original_warrior}") # Original unchanged
When to Use Each Pattern¶
| Pattern | Use When |
|---|---|
| Singleton | You need exactly one instance (logging, config) |
| Factory Method | You need flexibility in object creation |
| Abstract Factory | You need to create families of related objects |
| Builder | You need to construct complex objects step-by-step |
| Prototype | Copying is cheaper than creating from scratch |
Python-Specific Considerations¶
- Singleton: Python's module system naturally implements the Singleton pattern—modules are singletons by default.
- Factory Method: Python's duck typing makes factories even more flexible than in statically-typed languages.
- Builder: Method chaining (fluent interface) works beautifully in Python for the Builder pattern.
- Prototype: Python's
copymodule makes implementing prototypes straightforward.
Best Practices¶
- Don't overuse patterns: Apply them when they solve real problems, not for the sake of using patterns.
- Keep it Pythonic: Python's dynamic nature often provides simpler alternatives to traditional patterns.
- Test thoroughly: Patterns add abstraction layers—ensure they're well-tested.
- Document your intent: Make it clear why you're using a particular pattern.
Conclusion¶
Creational design patterns are powerful tools in a Python developer's toolkit. They promote loose coupling, improve code organization, and make systems more maintainable. Start by identifying repetitive object creation logic in your codebase and consider which pattern might help.