Creational Patterns I: Singleton & Factory Method
Design Patterns
Chapter 2 · Creational Patterns I: Singleton & Factory Method
Creational patterns control how objects get created. This chapter covers two of the most common — and Singleton, one of the most commonly misused — with real pitfalls reproduced directly, not just described.
Singleton: Guaranteeing Exactly One Instance
Ordinarily, calling a class's constructor twice gives two separate objects:
a is b evaluates to False — two genuinely separate objects, with different memory addresses.
Singleton overrides object creation itself so every call returns the same object:
x = SingletonLogger() and y = SingletonLogger(): x is y evaluates to True — identical memory address both times.
Pitfall 1: Singleton State Silently Pollutes Later Code
A shared global instance means shared global state — and that state doesn't reset itself between unrelated uses.
SingletonCounter with an increment() method: a first routine increments it three times and correctly finds count == 3. A second, entirely independent routine — written expecting a fresh counter — calls SingletonCounter(), increments once, and expects count == 1. It actually gets count == 4: the leftover state from the first routine never went away, because both routines received the exact same object.
Pitfall 2: A Naive Singleton Isn't Thread-Safe
The lazy-initialization check, if cls._instance is None:, has a real gap: two threads can both evaluate that check as True before either one finishes creating the instance.
20 threads simultaneously, each calling a naive lazy Singleton (with a small artificial delay inserted between the check and the creation, to widen the race window): instead of 1 shared instance, 8 genuinely distinct instances were created — confirmed by comparing each thread's own object identity. The "only one instance" guarantee failed completely under real concurrent access.
1 distinct instance, every time.
A Python-Specific Note: You Often Already Have a Singleton
import os followed by import os as os2: os is os2 evaluates to True. Python caches every imported module in sys.modules, so importing the same module anywhere, any number of times, always returns the identical object — a real, verified, zero-code Singleton, already built into the language. A module-level instance (a plain object created once at the top of a module) is often the more idiomatic Python choice over a hand-rolled __new__-based class, for exactly this reason.
Factory Method: Delegating "Which Class?" to a Subclass
Factory Method defines a creation method in a base class without deciding what it creates — each subclass overrides that one method to supply a different concrete type, while every other method in the base class works entirely through the shared interface.
EmailNotificationCreator().notify('Your order shipped') returns 'Email sent: Your order shipped'; SMSNotificationCreator().notify(...) returns 'SMS sent: Your order shipped' — the identical notify() method, defined once in the base class, correctly produces different behavior depending purely on which subclass's own create_notification() ran.
PushNotificationCreator — a new subclass with its own create_notification() returning a new PushNotification class — works correctly ('Push sent: Your order shipped') with zero changes to NotificationCreator.notify() itself, exactly the payoff Chapter 1 first demonstrated.
Where This Connects
| This chapter's finding | What it sets up |
|---|---|
| Singleton's verified test-pollution and race-condition failures | A concrete case study for Clean Code, SOLID & Refactoring's own treatment of hidden global state and testability |
| Factory Method's zero-change extensibility, verified | The same shape reappears at a larger scale in Chapter 3's Abstract Factory |
| Double-checked locking as a real, verified fix | A concrete instance of the "shortcuts need a checkable justification" discipline pseudocode1's own Chapter 7 established for greedy algorithms |
Hands-On Exercises
Using this chapter's own SingletonCounter example, explain what value a third, independent routine would see if it called SingletonCounter() and read count immediately, without calling increment() at all, after both routines from this chapter's own example have already run.
Using this chapter's own verified thread-safety findings, explain in your own words why the naive Singleton's race condition specifically requires MULTIPLE threads calling it for the very first time (before any instance exists yet) — and why calling an already-fully-initialized Singleton from multiple threads afterward would not have the same problem.
📄 View solutionUsing this chapter's own Factory Method example, write a new SlackNotification class and a SlackNotificationCreator subclass, following the same pattern as EmailNotification/EmailNotificationCreator. Verify it works correctly through the existing, unmodified notify() method.
Chapter 2 Quick Reference
- Singleton: a class that guarantees exactly one instance — verified via a
__new__override,x is yconfirmedTrue - Verified pitfall 1: shared Singleton state silently pollutes later, unrelated code — a second routine's "fresh" counter unexpectedly started at
3instead of0 - Verified pitfall 2: a naive lazy Singleton is not thread-safe — 20 concurrent threads produced
8distinct instances instead of1; double-checked locking fixed it to exactly1 - Verified: Python's own module system (
sys.modules) is already a built-in Singleton — often the more idiomatic choice over a hand-rolled class - Factory Method: a base class delegates "which concrete type?" to an overridden method in each subclass — verified correct behavior and zero-change extensibility, mirroring Chapter 1's own findings
- Next chapter: Creational Patterns II — Abstract Factory and Builder