86.2 Creational Patterns: Singleton, Factory, Abstract Factory, Builder
Right, creational patterns. This is where we stop just letting objects fall out of the sky and start putting on our grown-up pants, thinking about how these objects come into being. Because just slapping MyClass() everywhere is like trying to furnish your house exclusively with IKEA flat-packs and a hope and a prayer. Sometimes you need a custom cabinet maker. Or at least someone who knows which way the little Allen key turns.
These patterns exist to give you control over the object creation process, making your code more flexible, more testable, and less likely to collapse into a tangled mess of dependencies. Let’s get into it.
The Singleton: The Class That’s Really Just a Fancy Module
Ah, the Singleton. The most misunderstood and frequently abused pattern in the book. The goal is simple: ensure a class has only one instance and provide a global point of access to it. The implementation in Python is… well, it’s a trap for the unwary.
The classic Java-style way involves private constructors and static methods. We don’t do that here. Python is a consenting adult language. The simplest, most Pythonic way is to just use a module. Seriously. Modules are imported only once, are cached in sys.modules, and are globally accessible. They are, by definition, singletons. But if you absolutely must make a class singletonsque, here’s how you do it without summoning Cthulhu.
class OnlyOne:
_instance = None
def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
# Initialize here, not in __init__!
cls._instance.value = "I'm the only one"
return cls._instance
def __init__(self):
# Warning: __init__ gets called every time you instantiate,
# even though __new__ returns the same instance!
# This is why we set 'value' in __new__.
pass
# Let's test this monstrosity
first = OnlyOne()
second = OnlyOne()
print(first is second) # True. It's the same object.
print(first.value) # "I'm the only one"
The Pitfall: Notice the __init__ problem? It’s a landmine. Every time you call OnlyOne(), __init__ will blissfully re-run, potentially resetting the state of your precious single instance. This is why you often see the “Borg” or “Monostate” pattern in Python, where instances share state instead of enforcing identity. But for a true singleton, overriding __new__ is the way, just be hyper-aware of that __init__ quirk.
The Factory Method: Deferring Instantiation to Subclasses
The Factory Method is all about letting subclasses decide which class to instantiate. Think of it as outsourcing the new keyword. You define an interface for creating an object, but let the subclasses alter the type of objects that will be created.
Why? Because it introduces a layer of indirection between your code and the concrete classes it uses. Your high-level code doesn’t need to know about ConcreteProductA or ConcreteProductB; it just talks to the Product interface. This makes your code blissfully unaware of the specific classes it’s using, which is a hallmark of maintainable design.
from abc import ABC, abstractmethod
class Logistics(ABC):
# This is the Factory Method.
@abstractmethod
def create_transport(self):
pass
def plan_delivery(self):
# The core business logic doesn't care about the transport.
transport = self.create_transport()
return f"Planning delivery with {transport.deliver()}"
class RoadLogistics(Logistics):
def create_transport(self):
return Truck() # Returns a Concrete Product
class SeaLogistics(Logistics):
def create_transport(self):
return Ship() # Returns a different Concrete Product
class Transport(ABC):
@abstractmethod
def deliver(self):
pass
class Truck(Transport):
def deliver(self):
return " delivering by land in a box."
class Ship(Transport):
def deliver(self):
return " delivering by sea in a container."
# Client code works with the Logistics interface.
logistics = RoadLogistics()
print(logistics.plan_delivery()) # Planning delivery with delivering by land in a box.
The beauty here is that plan_delivery is completely decoupled from the Truck or Ship classes. To change the entire delivery method, you just swap the factory.
The Abstract Factory: Factories of Factories
If the Factory Method creates one type of object, the Abstract Factory creates families of related or dependent objects. You have an interface with multiple factory methods, and each concrete factory produces objects that belong to a single theme or style.
The classic example is a UI toolkit. You need buttons, text boxes, and dialogs, but they all need to look like they’re from the same operating system.
class GUIFactory(ABC):
@abstractmethod
def create_button(self):
pass
@abstractmethod
def create_dialog(self):
pass
class WindowsFactory(GUIFactory):
def create_button(self):
return WindowsButton()
def create_dialog(self):
return WindowsDialog()
class MacOSFactory(GUIFactory):
def create_button(self):
return MacOSButton()
def create_dialog(self):
return MacOSDialog()
# The products
class Button(ABC):
@abstractmethod
def click(self):
pass
class WindowsButton(Button):
def click(self):
return "Windows button clicked. It makes a satisfying 'clunk'."
class MacOSButton(Button):
def click(self):
return "MacOS button clicked. It's unnervingly smooth."
def create_ui(factory: GUIFactory):
# This client code is completely independent of concrete classes.
button = factory.create_button()
dialog = factory.create_dialog()
print(button.click())
print(dialog.open())
# Create a Windows-themed UI
create_ui(WindowsFactory())
The key insight is that WindowsFactory and MacOSFactory guarantee that you’ll never get a MacOS button mixed with a Windows dialog. They enforce consistency across the product family.
The Builder: Taming the Constructor with Too Many Parameters
Ever seen a class with a constructor that looks like this?
def __init__(self, a, b, c, d, e, f, g=None, h=None, i=True, j=False):
Yeah, me too. It’s a nightmare. The Builder pattern separates the construction of a complex object from its representation. It lets you create a complex object step-by-step without having to cram 20 parameters into a constructor.
class Pizza:
def __init__(self):
self.size = None
self.cheese = False
self.pepperoni = False
self.mushrooms = False
# ... and 10 other toppings
def __str__(self):
return f"A {self.size} pizza with {', '.join([k for k, v in self.__dict__.items() if v is True])}"
class PizzaBuilder:
def __init__(self, size):
self.pizza = Pizza()
self.pizza.size = size
def add_cheese(self):
self.pizza.cheese = True
return self # This is the magic: returning self for method chaining.
def add_pepperoni(self):
self.pizza.pepperoni = True
return self
def add_mushrooms(self):
self.pizza.mushrooms = True
return self
def build(self):
return self.pizza
# Now, building a pizza is a readable, step-by-step process.
my_pizza = (PizzaBuilder('large')
.add_cheese()
.add_pepperoni()
.add_mushrooms()
.build())
print(my_pizza) # A large pizza with cheese, pepperoni, mushrooms
This is infinitely clearer than Pizza('large', True, True, False, False, True, ...). You can also have a Director class that knows the “recipes” for specific types of pizzas (e.g., director.make_veggie_supreme()), which encapsulates the construction logic even further. The Builder pattern is your best friend when dealing with objects that have a dizzying number of configuration options.