This lesson on Design Patterns in Python is hands-on and example-driven. You will identify, classify, and implement foundational Gang of Four design patterns to resolve recurring architectural problems. You will evaluate structural, creational, and behavioral trade-offs to structure application code cleanly without adding unnecessary boilerplate.
What You'll Be Able To Do
- Categorize a given software design pattern into Creational, Structural, or Behavioral domains.
- Implement the Singleton, Factory, Builder, and Prototype creational patterns using modern language constructs.
- Construct Facade and Proxy structural wrappers to simplify subsystems and intercept runtime operations.
- Implement custom Iterator protocols and Observer push-based subscription models for complex data flows.
Detailed Concept Walkthrough
1. Creational Patterns: Object Instantiation
Creational patterns abstract the instantiation process to make systems independent of how their objects are created, composed, and represented.
- Mechanism: Singleton restricts a class to one instance with a global access point; Factory abstracts runtime class selection; Builder configures complex objects step-by-step; Prototype creates new objects by cloning an existing prototype object.
- Execution Flow: The Builder pattern returns the instance context via method chaining (
this), while the Factory function inspects execution conditions to instantiate and return the matching subclass. - Best Practice: Use language-native capabilities when possible, such as
Object.create()for prototype-based inheritance or standard object literals instead of heavy Singleton boilerplate.
// Builder with method chaining and Factory conditional creation
class RequestBuilder {
constructor() { this.options = {}; }
setMethod(method) { this.options.method = method; return this; }
setUrl(url) { this.options.url = url; return this; }
build() { return this.options; }
}
const getReq = new RequestBuilder().setMethod('GET').setUrl('/api').build();
Key Takeaway: Creational patterns isolate creation logic from operational business logic, simplifying instantiation variants and complex configuration.
2. Structural Patterns: Composition and Interception
Structural patterns manage relationships between entities to form larger, flexible architectures without coupling concrete subsystems.
- Mechanism: Facade provides a simplified high-level interface over multiple interdependent subsystems; Proxy wraps a target object to intercept and control member access.
- Under the Hood: A Proxy utilizes runtime interceptor handlers (
get,set) to execute auxiliary operations such as access control, caching, or triggering UI rerenders before delegating back to the target. - Best Practice: Use Facades to decouple client consumers from internal subsystem restructuring, and use Proxies for non-intrusive cross-cutting concerns like reactivity or lazy loading.
// Proxy pattern intercepting state updates
const state = { count: 0 };
const proxyState = new Proxy(state, {
set(target, prop, value) {
console.log(`Setting ${prop} to ${value}`);
target[prop] = value; // update underlying target
return true;
}
});
proxyState.count = 1; // triggers console log and updates value
Key Takeaway: Structural patterns organize object relationships, using Facade for interface simplification and Proxy for transparent access interception.
3. Behavioral Patterns: Iteration and Notification
Behavioral patterns structure the communication, state distribution, and control flow between distributed software components.
- Mechanism: Iterator standardizes sequential collection traversal via a pull-based
next()contract; Observer establishes a push-based one-to-many relationship where subjects broadcast state changes to subscribers. - Execution Flow: Iterators maintain internal traversal state and return a payload with
valueanddoneproperties; Observers register callback functions that the subject synchronously or asynchronously invokes upon state changes. - Best Practice: Implement native iterable protocols (like
Symbol.iterator) for universal loop compatibility, and use reactive subjects for decoupled real-time event broadcasting.
// Iterator implementing custom traversal protocol
const range = {
from: 1, to: 3,
[Symbol.iterator]() {
let current = this.from;
return {
next: () => (current <= this.to ? { value: current++, done: false } : { done: true })
};
}
};
for (const num of range) { console.log(num); } // logs 1, 2, 3
Key Takeaway: Behavioral patterns establish clear communication contracts between objects, separating data consumption (pull) from event broadcasting (push).
4. Pattern Taxonomy and Architectural Pragmatism
Design patterns provide proven architectural templates across Creational, Structural, and Behavioral categories, but must be applied judiciously.
- Mechanism: Patterns organize architectural intent into three distinct categories based on whether they solve instantiation, composition, or interaction problems.
- Under the Hood: Over-applying patterns introduces unnecessary indirection and boilerplate, creating architectural rigidity termed the 'Sistine Chapel' problem.
- Best Practice: Prefer native language idioms and simple implementations before introducing formal pattern abstractions to keep the codebase maintainable.
// Prototype delegation via flat prototype chain
const protoCar = { drive() { return 'Vroom'; } };
const myCar = Object.create(protoCar);
console.log(myCar.drive()); // delegates lookup to protoCar via __proto__
Key Takeaway: Apply design patterns only when specific architectural complexity demands structured abstraction.
Topics Covered in Design Patterns in Python
- Design Pattern Categories (0:00 - 1:31) — Introduces Gang of Four background and defines Creational, Structural, and Behavioral categories.
- Singleton Pattern (1:31 - 2:22) — Explains how to restrict class instantiation to a single global instance using static methods.
- Prototype Pattern (2:22 - 3:42) — Demonstrates cloning existing objects via prototype delegation and flat prototype chains.
- Builder Pattern (3:42 - 4:23) — Illustrates constructing complex objects step-by-step using method chaining returning instance references.
- Factory Pattern (4:23 - 4:53) — Shows how to delegate conditional object instantiation away from direct constructor calls.
- Facade Pattern (4:53 - 5:47) — Covers simplifying interactions with complex subsystems by wrapping them in a unified high-level interface.
- Proxy Pattern (5:47 - 6:37) — Explains wrapping objects with handler traps to intercept read and write operations for side effects.
- Iterator Pattern (6:37 - 7:51) — Walks through implementing custom pull-based sequence iteration with standard iterator protocol symbols.
- Observer Pattern (7:51 - 8:30) — Details push-based one-to-many communication systems using subscription and broadcast mechanisms.
Python Cheat Sheet
-
Singleton— Ensures single instance with static accessorclass S { static get instance() { return this._i || (this._i = new S()); } } -
Object.create(proto)— Creates object inheriting directly from prototypeconst child = Object.create(protoParent); -
Method Chaining (Builder)— Returns instance reference for sequential configurationsetVal(v) { this.v = v; return this; } -
Factory Function— Encapsulates conditional object instantiation logicconst createBtn = (os) => os === 'ios' ? new IOSBtn() : new DroidBtn(); -
Facade Class— Consolidates multiple subsystems into unified interfaceclass Facade { start() { this.sub1.on(); this.sub2.on(); } } -
new Proxy(target, handler)— Wraps target to intercept member operationsconst p = new Proxy(obj, { get: (t, k) => t[k] ?? 'default' }); -
Symbol.iterator— Defines custom iteration protocol for loops[Symbol.iterator]() { return { next: () => ({ done: true }) }; } -
Subject (Observer)— Maintains subscriber list and broadcasts updatesclass Subject { notify(d) { this.subs.forEach(fn => fn(d)); } }
Comparison Table
| Pattern Category | Primary Architectural Purpose | Data/Control Direction |
|---|---|---|
| Creational | Abstract object creation logic | Instantiation to caller |
| Structural | Simplify component composition relationships | Interception or facade encapsulation |
| Behavioral | Standardize communication and event propagation | Pull (Iterator) or Push (Observer) |
Common Pitfalls
- Mistake: Overusing the Singleton pattern in dynamic languages. Avoid: Using native object literals or module singletons to prevent unnecessary class boilerplate.
- Mistake: Forgetting to return the instance reference in Builder methods. Avoid: Returning this at the end of each chaining method to permit continuous configuration.
- Mistake: Applying complex design patterns prematurely to simple codebases. Avoid: Introducing formal patterns only when recurring structural complexity justifies the abstraction layer.
FAQs
- Why is the classic Singleton pattern often discouraged in dynamic languages? Module systems and top-level object literals already enforce single-instance state globally without requiring private constructors and static accessors.
- What is the key functional difference between Iterator and Observer? Iterator is a pull-based pattern where the consumer requests items sequentially, whereas Observer is a push-based pattern where the subject broadcasts updates to subscribers.
- How does a Proxy differ from a Facade? A Proxy maintains the same interface as its target object to intercept and control operations, while a Facade creates a new, simplified interface over multiple underlying subsystems.