Table of Contents
- What Are Creational Design Patterns?
- Singleton Pattern
- Factory Method Pattern
- Abstract Factory Pattern
- Builder Pattern
- Prototype Pattern
- Comparing Creational Patterns: When to Use Which?
- Conclusion
- References
What Are Creational Design Patterns?
Creational patterns deal with the creation of objects. They separate the process of object creation from the code that uses the objects, ensuring that your system remains flexible when it comes to how objects are generated.
The key goals of creational patterns include:
- Encapsulating knowledge about which classes get instantiated.
- Hiding how instances are created and composed.
- Promoting loose coupling by deferring instantiation to subclasses or helper objects.
In this guide, we’ll explore five fundamental creational patterns: Singleton, Factory Method, Abstract Factory, Builder, and Prototype.
Singleton Pattern
What It Is
The Singleton pattern ensures a class has exactly one instance and provides a global point of access to it. This is useful for scenarios where a single shared resource (e.g., a configuration manager or logger) is needed across an application.
Problem It Solves
Imagine you’re building a configuration manager that loads settings from a file. If multiple parts of your app create their own instance of this manager, you might end up with redundant file reads, inconsistent state, or wasted memory. The Singleton pattern prevents this by enforcing a single instance.
When to Use It
- When exactly one instance of a class is required (e.g., logging, thread pools, or database connections).
- When a global access point is needed to that instance.
Python Implementation
In Python, we can implement Singleton using the __new__ method (which controls object creation) to ensure only one instance is ever created.
Example: A Simple Configuration Manager
class ConfigManager:
_instance = None # Class-level variable to store the single instance
def __new__(cls):
# If no instance exists, create one; otherwise, return the existing one
if cls._instance is None:
cls._instance = super().__new__(cls)
# Initialize configuration (e.g., load from a file)
cls._instance.settings = {"theme": "dark", "notifications": True}
return cls._instance
def get_setting(self, key):
return self.settings.get(key)
# Usage
config1 = ConfigManager()
config2 = ConfigManager()
# Both variables point to the same instance
print(config1 is config2) # Output: True
# Access shared settings
print(config1.get_setting("theme")) # Output: dark
config2.settings["theme"] = "light"
print(config1.get_setting("theme")) # Output: light (changes reflect globally)
Key Notes
- The
_instancevariable is private (by convention, using a leading underscore) to prevent direct modification. __new__is called before__init__, so we initialize the instance’s state (e.g.,settings) inside__new__the first time it’s created.
Pros and Cons
| Pros | Cons |
|---|---|
| Ensures a single instance | Violates the Single Responsibility Principle (handles creation and business logic). |
| Global access point | Can make unit testing harder (tightly coupled to the singleton instance). |
| Reduces redundant resource usage | Not thread-safe by default (requires extra work for multi-threaded apps). |
Factory Method Pattern
What It Is
The Factory Method pattern defines an interface for creating objects but lets subclasses decide which class to instantiate. It delegates object creation to subclasses, promoting loose coupling between the creator (the class that uses the factory) and the products (the objects being created).
Problem It Solves
Suppose you’re building a document editor that supports multiple file types (e.g., text documents, spreadsheets, presentations). If you hardcode the creation of specific document types in the editor, adding a new type (e.g., PDFs) would require changing the editor’s code. The Factory Method pattern avoids this by letting subclasses handle object creation.
When to Use It
- When a class can’t anticipate the type of objects it needs to create.
- When you want to localize the knowledge of which class gets instantiated.
Key Components
- Product: The interface/abstract class defining the objects the factory creates (e.g.,
Document). - Concrete Product: The actual objects created by the factory (e.g.,
TextDocument,Spreadsheet). - Creator: The abstract class/interface with a factory method that returns a Product (e.g.,
DocumentEditor). - Concrete Creator: Subclasses of the Creator that override the factory method to create Concrete Products (e.g.,
TextEditor,SpreadsheetEditor).
Python Implementation
Example: A Document Editor with Pluggable Formats
from abc import ABC, abstractmethod
# ------------------------------
# Product Interface (Document)
# ------------------------------
class Document(ABC):
@abstractmethod
def open(self):
pass
# ------------------------------
# Concrete Products
# ------------------------------
class TextDocument(Document):
def open(self):
return "Opening text document ( .txt )"
class SpreadsheetDocument(Document):
def open(self):
return "Opening spreadsheet ( .xlsx )"
# ------------------------------
# Creator Interface (DocumentEditor)
# ------------------------------
class DocumentEditor(ABC):
@abstractmethod
def create_document(self): # Factory Method
pass
def open_document(self):
# Use the factory method to create a document, then open it
doc = self.create_document()
return doc.open()
# ------------------------------
# Concrete Creators
# ------------------------------
class TextEditor(DocumentEditor):
def create_document(self):
return TextDocument() # Creates a TextDocument
class SpreadsheetEditor(DocumentEditor):
def create_document(self):
return SpreadsheetDocument() # Creates a SpreadsheetDocument
# ------------------------------
# Client Code
# ------------------------------
def main():
# Open a text document
text_editor = TextEditor()
print(text_editor.open_document()) # Output: Opening text document ( .txt )
# Open a spreadsheet
spreadsheet_editor = SpreadsheetEditor()
print(spreadsheet_editor.open_document()) # Output: Opening spreadsheet ( .xlsx )
if __name__ == "__main__":
main()
Key Notes
- The
DocumentEditor(Creator) defines thecreate_documentfactory method, but leaves the actual creation to subclasses likeTextEditor. - Adding a new document type (e.g.,
PdfDocument) only requires:- Creating a new
PdfDocumentclass (Concrete Product). - Creating a
PdfEditorclass (Concrete Creator) that overridescreate_document.
- Creating a new
- The client code (
main) works with the abstractDocumentEditorandDocumentinterfaces, making it decoupled from concrete types.
Pros and Cons
| Pros | Cons |
|---|---|
| Promotes loose coupling (client code depends on abstractions, not concretions). | Adds complexity (requires creating multiple classes: Creator, Concrete Creators, Products). |
| Follows the Open/Closed Principle (easy to add new products without changing existing code). | Overkill for simple cases with few product types. |
Abstract Factory Pattern
What It Is
The Abstract Factory pattern provides an interface for creating families of related or dependent objects without specifying their concrete classes. Unlike the Factory Method (which focuses on one product), Abstract Factory handles multiple related products.
Problem It Solves
Imagine building a GUI toolkit that supports two themes: Windows and macOS. Each theme has its own style of buttons, checkboxes, and text fields. If you hardcode these widgets, switching themes would require rewriting large portions of code. The Abstract Factory pattern lets you create theme-specific widget families without coupling to concrete types.
When to Use It
- When you need to create objects that belong to a family (e.g., UI components for different platforms).
- When you want to enforce consistency among related objects (e.g., a Windows button should pair with a Windows checkbox).
Key Components
- Abstract Factory: Defines methods for creating each product in the family (e.g.,
create_button(),create_checkbox()). - Concrete Factory: Implements the Abstract Factory to create products for a specific family (e.g.,
WindowsFactory,MacOSFactory). - Abstract Product: Defines the interface for a product type (e.g.,
Button,Checkbox). - Concrete Product: Implements the Abstract Product for a specific family (e.g.,
WindowsButton,MacOSCheckbox).
Python Implementation
Example: A GUI Toolkit with Themes
from abc import ABC, abstractmethod
# ------------------------------
# Abstract Products (Widgets)
# ------------------------------
class Button(ABC):
@abstractmethod
def render(self):
pass
class Checkbox(ABC):
@abstractmethod
def render(self):
pass
# ------------------------------
# Concrete Products (Windows Theme)
# ------------------------------
class WindowsButton(Button):
def render(self):
return "Rendering Windows-style button (gray, square)"
class WindowsCheckbox(Checkbox):
def render(self):
return "Rendering Windows-style checkbox (square, labeled)"
# ------------------------------
# Concrete Products (macOS Theme)
# ------------------------------
class MacOSButton(Button):
def render(self):
return "Rendering macOS-style button (blue, rounded)"
class MacOSCheckbox(Checkbox):
def render(self):
return "Rendering macOS-style checkbox (round, unlabeled)"
# ------------------------------
# Abstract Factory (GUI Factory)
# ------------------------------
class GUIFactory(ABC):
@abstractmethod
def create_button(self):
pass
@abstractmethod
def create_checkbox(self):
pass
# ------------------------------
# Concrete Factories (Theme-Specific)
# ------------------------------
class WindowsFactory(GUIFactory):
def create_button(self):
return WindowsButton()
def create_checkbox(self):
return WindowsCheckbox()
class MacOSFactory(GUIFactory):
def create_button(self):
return MacOSButton()
def create_checkbox(self):
return MacOSCheckbox()
# ------------------------------
# Client Code
# ------------------------------
def build_ui(factory: GUIFactory):
# Create a family of widgets using the factory
button = factory.create_button()
checkbox = factory.create_checkbox()
print(button.render())
print(checkbox.render())
# Build UI for Windows
print("Windows Theme:")
build_ui(WindowsFactory())
# Output:
# Rendering Windows-style button (gray, square)
# Rendering Windows-style checkbox (square, labeled)
# Build UI for macOS
print("\nmacOS Theme:")
build_ui(MacOSFactory())
# Output:
# Rendering macOS-style button (blue, rounded)
# Rendering macOS-style checkbox (round, unlabeled)
Key Notes
- The
GUIFactory(Abstract Factory) defines methods for creating buttons and checkboxes. Concrete factories (WindowsFactory,MacOSFactory) produce theme-specific widgets. - All products in a family are guaranteed to be compatible (e.g.,
WindowsFactoryonly creates Windows-style widgets). - To add a new theme (e.g., Linux), you’d create a
LinuxFactoryand correspondingLinuxButton,LinuxCheckboxclasses—no changes to existing code required.
Pros and Cons
| Pros | Cons |
|---|---|
| Ensures consistency among related products (e.g., all widgets in a theme match). | Complexity increases with more product types (many classes to manage). |
| Isolates concrete classes (client code never directly creates products). | Hard to add new product types (e.g., adding a Slider would require modifying the Abstract Factory and all Concrete Factories). |
Builder Pattern
What It Is
The Builder pattern separates the construction of a complex object from its representation, allowing the same construction process to create different representations. It’s ideal for objects with many optional components or configurations.
Problem It Solves
Consider building a “meal kit” that can include a burger, drink, side, and dessert. Some customers want a vegan meal (veggie burger, soda, fries), others a non-vegan meal (beef burger, milkshake, onion rings). If you hardcode these combinations, the code becomes bloated with conditionals. The Builder pattern lets you construct these meals step-by-step using specialized builders.
When to Use It
- When creating complex objects with many optional parts (e.g., meal kits, car configurations, or HTML documents).
- When you want to reuse the same construction logic to build different representations.
Key Components
- Builder: Defines steps to build a product (e.g.,
build_burger(),build_drink()). - Concrete Builder: Implements the Builder steps to create a specific product (e.g.,
VegMealBuilder,NonVegMealBuilder). - Director: Orchestrates the construction process using a Builder (e.g.,
MealDirectorthat callsbuild_burger(), thenbuild_drink()). - Product: The complex object being built (e.g.,
Meal).
Python Implementation
Example: Building Custom Meal Kits
from abc import ABC, abstractmethod
# ------------------------------
# Product (Meal)
# ------------------------------
class Meal:
def __init__(self):
self.burger = None
self.drink = None
self.side = None
def __str__(self):
return f"Meal: {self.burger}, {self.drink}, {self.side}"
# ------------------------------
# Builder (Abstract)
# ------------------------------
class MealBuilder(ABC):
@abstractmethod
def build_burger(self):
pass
@abstractmethod
def build_drink(self):
pass
@abstractmethod
def build_side(self):
pass
@abstractmethod
def get_meal(self):
pass
# ------------------------------
# Concrete Builders (Meal Types)
# ------------------------------
class VegMealBuilder(MealBuilder):
def __init__(self):
self.meal = Meal() # Create an empty meal
def build_burger(self):
self.meal.burger = "Veggie Burger"
def build_drink(self):
self.meal.drink = "Orange Soda"
def build_side(self):
self.meal.side = "Sweet Potato Fries"
def get_meal(self):
return self.meal
class NonVegMealBuilder(MealBuilder):
def __init__(self):
self.meal = Meal()
def build_burger(self):
self.meal.burger = "Beef Burger"
def build_drink(self):
self.meal.drink = "Chocolate Milkshake"
def build_side(self):
self.meal.side = "Onion Rings"
def get_meal(self):
return self.meal
# ------------------------------
# Director (Orchestrates Construction)
# ------------------------------
class MealDirector:
def __init__(self, builder: MealBuilder):
self.builder = builder
def construct_meal(self):
# Step-by-step construction
self.builder.build_burger()
self.builder.build_drink()
self.builder.build_side()
# ------------------------------
# Client Code
# ------------------------------
def main():
# Build a vegan meal
veg_builder = VegMealBuilder()
director = MealDirector(veg_builder)
director.construct_meal()
veg_meal = veg_builder.get_meal()
print(veg_meal) # Output: Meal: Veggie Burger, Orange Soda, Sweet Potato Fries
# Build a non-vegan meal
non_veg_builder = NonVegMealBuilder()
director.builder = non_veg_builder # Reuse the director with a new builder
director.construct_meal()
non_veg_meal = non_veg_builder.get_meal()
print(non_veg_meal) # Output: Meal: Beef Burger, Chocolate Milkshake, Onion Rings
if __name__ == "__main__":
main()
Key Notes
- The
MealBuilderinterface defines steps to build a meal, but theVegMealBuilderandNonVegMealBuilderhandle the actual details (e.g., which burger or drink to use). - The
MealDirectorcontrols the construction order (e.g., always build the burger first, then the drink). - Clients can even skip the Director and build products directly using a Builder (e.g., for custom meals with only a burger and drink).
Pros and Cons
| Pros | Cons |
|---|---|
| Separates construction logic from product representation (cleaner code). | Adds complexity (requires creating Builder, Concrete Builders, and Director classes). |
| Allows creating different product variations using the same construction process. | Overkill for simple objects with few parts. |
Prototype Pattern
What It Is
The Prototype pattern specifies the kinds of objects to create using a prototypical instance and creates new objects by copying (cloning) this prototype. It’s useful when creating new objects is expensive (e.g., requires database calls) or when you need many similar objects with slight variations.
Problem It Solves
Suppose you’re developing a game with hundreds of enemies. Each enemy type (e.g., “Goblin”, “Orc”) has base properties (health, speed, damage) but may have unique colors or weapons. Creating each enemy from scratch would be slow (e.g., reloading base stats from a database). The Prototype pattern lets you clone a base “prototype” enemy and tweak its properties, saving time.
When to Use It
- When object creation is costly (e.g., requires heavy setup or external resources).
- When you need to create many objects with similar properties (e.g., game entities, documents with templates).
Key Components
- Prototype: Defines a
clone()method (e.g.,Shapewithclone()). - Concrete Prototype: Implements
clone()to copy itself (e.g.,Circle,Square). - Client: Creates new objects by cloning prototypes.
Python Implementation
In Python, cloning can be done using copy.copy() (shallow copy) or copy.deepcopy() (deep copy, for nested objects). We’ll use deepcopy to ensure cloned objects are fully independent.
Example: Cloning Game Characters
import copy
from abc import ABC, abstractmethod
# ------------------------------
# Prototype Interface
# ------------------------------
class CharacterPrototype(ABC):
@abstractmethod
def clone(self):
pass
@abstractmethod
def __str__(self):
pass
# ------------------------------
# Concrete Prototypes (Character Types)
# ------------------------------
class Goblin(CharacterPrototype):
def __init__(self, health=100, speed=2, color="green"):
self.health = health
self.speed = speed
self.color = color # Unique property for Goblins
def clone(self):
# Return a deep copy to avoid sharing nested data
return copy.deepcopy(self)
def __str__(self):
return f"Goblin: Health={self.health}, Speed={self.speed}, Color={self.color}"
class Orc(CharacterPrototype):
def __init__(self, health=200, speed=1, weapon="axe"):
self.health = health
self.speed = speed
self.weapon = weapon # Unique property for Orcs
def clone(self):
return copy.deepcopy(self)
def __str__(self):
return f"Orc: Health={self.health}, Speed={self.speed}, Weapon={self.weapon}"
# ------------------------------
# Client Code
# ------------------------------
def main():
# Create base prototypes
base_goblin = Goblin() # Default: health=100, speed=2, color=green
base_orc = Orc() # Default: health=200, speed=1, weapon=axe
# Clone prototypes to create new characters with variations
red_goblin = base_goblin.clone()
red_goblin.color = "red"
red_goblin.health = 120 # Buffed red goblin
spear_orc = base_orc.clone()
spear_orc.weapon = "spear"
spear_orc.speed = 2 # Faster orc with a spear
# Print results
print(base_goblin) # Output: Goblin: Health=100, Speed=2, Color=green
print(red_goblin) # Output: Goblin: Health=120, Speed=2, Color=red
print(base_orc) # Output: Orc: Health=200, Speed=1, Weapon=axe
print(spear_orc) # Output: Orc: Health=200, Speed=2, Weapon=spear
if __name__ == "__main__":
main()
Key Notes
copy.deepcopy()ensures nested attributes (e.g., if aGoblinhad askillslist) are copied, not referenced. Usecopy.copy()for shallow copies (faster but risks shared data).- Prototypes act as “templates”:
base_goblinandbase_orcdefine default values, and clones are customized. - Adding a new character type (e.g.,
Elf) only requires creating anElfclass that inheritsCharacterPrototypeand implementsclone().
Pros and Cons
| Pros | Cons |
|---|---|
| Reduces object creation costs (cloning is cheaper than reinitializing). | Cloning complex objects with many nested references can be error-prone (requires careful use of deep/shallow copies). |
| Simplifies adding new object types (just create a new Concrete Prototype). | May hide the complexity of object initialization (harder to debug if prototypes are misconfigured). |
Comparing Creational Patterns: When to Use Which?
| Pattern | Use Case | Key Idea |
|---|---|---|
| Singleton | Ensure one instance of a class (e.g., logging, config). | ”One and only one.” |
| Factory Method | Delegate object creation to subclasses (e.g., plugin systems). | ”Let subclasses decide what to create.” |
| Abstract Factory | Create families of related objects (e.g., UI themes). | ”Families of products.” |
| Builder | Construct complex objects step-by-step (e.g., meal kits, car configs). | ”Separate construction from representation.” |
| Prototype | Clone existing objects to avoid expensive creation (e.g., game entities). | ”Copy, don’t create.” |
Conclusion
Creational design patterns are powerful tools for managing object creation in Python. By abstracting how objects are instantiated, they make your code more flexible, maintainable, and scalable.
- Start simple: Use Singleton for single-instance needs, Factory Method for basic object creation delegation.
- Scale up: Use Abstract Factory for related product families, Builder for complex objects, and Prototype for expensive or repetitive object creation.
The key is to recognize when object creation logic is becoming messy or rigid—and then apply the right pattern to clean it up. With practice, these patterns will become second nature!
References
- Gamma, E., Helm, R., Johnson, R., & Vlissides, J. (1994). Design Patterns: Elements of Reusable Object-Oriented Software. Addison-Wesley.
- Python Documentation: abc — Abstract Base Classes
- Real Python: Design Patterns in Python
- GitHub: Python Design Patterns Repository (examples of all patterns in Python).