Chapter 25: Exercises — Design Patterns and Clean Code
These 30 exercises are organized into five tiers of increasing difficulty. Each tier builds on the skills developed in previous tiers.
Tier 1: Recall and Recognition (Exercises 1–6)
Exercise 1: Pattern Identification
Read the following code snippets and identify which design pattern each one implements:
Snippet A:
class Logger:
_instance = None
def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
Snippet B:
handlers = {"csv": parse_csv, "json": parse_json, "xml": parse_xml}
def parse(format_type, data):
return handlers[format_type](data)
Snippet C:
class OldAPI:
def fetch_data(self, query):
...
class NewAPIAdapter:
def __init__(self, old_api):
self._api = old_api
def get(self, params):
return self._api.fetch_data(params["query"])
Task: Name the pattern for each snippet and explain your reasoning in 2–3 sentences per snippet.
Exercise 2: Clean Code Principles Matching
Match each clean code principle to its definition:
| Principle | Definition |
|---|---|
| A. DRY | 1. Functions should do one thing and do it well |
| B. KISS | 2. Leave the code better than you found it |
| C. SRP | 3. Every piece of knowledge should have a single representation |
| D. Boy Scout Rule | 4. Choose the simplest solution that works |
| E. YAGNI | 5. Don't build features until they are needed |
Exercise 3: Code Smell Identification
Identify at least three code smells in the following function:
def f(d, t, s, m, r):
# process the data
result = []
for i in range(len(d)):
if d[i]["type"] == t:
if d[i]["status"] == s:
if d[i]["mode"] == m:
x = d[i]["value"] * r
result.append({"id": d[i]["id"], "value": x, "type": t, "status": s, "mode": m})
return result
List each smell and explain why it is a problem.
Exercise 4: Protocol vs ABC
Explain the difference between typing.Protocol and abc.ABC in Python. Give one scenario where each would be the better choice. Write a short code example for each (5–10 lines).
Exercise 5: Pattern Vocabulary
For each of the following scenarios, name the most appropriate design pattern:
- You need to convert data from an old system's format to work with a new API.
- You want to add logging to every method call without modifying existing classes.
- Different shipping carriers each have their own rate calculation formula.
- A spreadsheet application needs to recalculate formulas when cell values change.
- You are building SQL queries step by step with optional clauses.
- You want a simple interface to a complex image-processing library.
Exercise 6: Pythonic vs Non-Pythonic
Rewrite each Java-style Python snippet in idiomatic Python:
Snippet A:
class StringUtils:
@staticmethod
def is_empty(s):
if s is None:
return True
if len(s) == 0:
return True
return False
Snippet B:
class NumberList:
def __init__(self):
self.numbers = []
def addNumber(self, number):
self.numbers.append(number)
def getSum(self):
total = 0
for i in range(len(self.numbers)):
total = total + self.numbers[i]
return total
Tier 2: Understanding and Application (Exercises 7–12)
Exercise 7: Simple Factory Implementation
Create a factory function create_notification that produces different notification objects based on a channel parameter. Support "email", "sms", and "push". Each notification type should have a send(recipient: str, message: str) -> bool method.
Requirements:
- Use a Notification Protocol
- Use dataclasses for each concrete type
- Use dictionary dispatch in the factory
- Include type hints throughout
Exercise 8: Decorator Pattern — Both Kinds
Implement both forms of the Decorator pattern:
Part A: Write a Python decorator (function-based) called @timed that logs the execution time of any function.
Part B: Write a GoF-style Decorator pattern with a MessageProcessor protocol and concrete decorators EncryptionDecorator and CompressionDecorator that can be stacked.
Exercise 9: Observer with Type Safety
Implement an EventBus class that:
- Allows subscribing to events by event type (using the class of a dataclass)
- Dispatches events only to handlers registered for that specific event type
- Uses type hints throughout
- Includes an unsubscribe method
Test it with at least two different event types.
Exercise 10: Clean Code Refactoring
Refactor the following code to follow clean code principles. Preserve the exact behavior.
def do_stuff(data):
r = []
for d in data:
if d["t"] == "A":
v = d["v"] * 1.1
if v > 100:
v = 100
r.append({"id": d["id"], "val": v, "cat": "premium"})
elif d["t"] == "B":
v = d["v"] * 0.9
if v < 0:
v = 0
r.append({"id": d["id"], "val": v, "cat": "standard"})
elif d["t"] == "C":
v = d["v"] * 0.5
r.append({"id": d["id"], "val": v, "cat": "discount"})
return r
Your refactored version should: - Use descriptive names - Extract functions where appropriate - Eliminate duplication - Add type hints
Exercise 11: Builder for Configuration
Create a ServerConfigBuilder class that builds a ServerConfig dataclass. The builder should support:
- Setting host and port
- Adding middleware (list of strings)
- Setting SSL certificate and key paths
- Setting environment (development, staging, production)
- A build() method that validates the configuration (production requires SSL, port must be > 0)
Exercise 12: Facade Pattern
Design a ReportGenerator facade that simplifies the following subsystem:
class DataFetcher:
def fetch(self, query: str) -> list[dict]: ...
class DataCleaner:
def remove_nulls(self, data: list[dict]) -> list[dict]: ...
def normalize(self, data: list[dict], columns: list[str]) -> list[dict]: ...
class StatisticsEngine:
def compute_summary(self, data: list[dict]) -> dict: ...
def compute_correlations(self, data: list[dict], columns: list[str]) -> dict: ...
class ChartRenderer:
def bar_chart(self, data: dict, title: str) -> str: ...
def line_chart(self, data: list[dict], x: str, y: str) -> str: ...
class PDFExporter:
def create_document(self, title: str) -> None: ...
def add_text(self, text: str) -> None: ...
def add_chart(self, chart: str) -> None: ...
def export(self, filename: str) -> str: ...
Your facade should have a single generate_report(query, title, filename) method.
Tier 3: Analysis and Integration (Exercises 13–18)
Exercise 13: Strategy Pattern with Registration
Extend the Strategy pattern to support dynamic registration of strategies at runtime. Create a TextAnalyzer class that:
- Has a registry of analysis strategies (word count, sentence count, readability score, keyword extraction)
- Allows registering new strategies via a register_strategy class method
- Can run all registered strategies or a selected subset on a given text
- Returns results as a dictionary mapping strategy names to results
Write at least four concrete strategies and demonstrate dynamic registration.
Exercise 14: Command Pattern with Macro Support
Extend the Command pattern from the chapter to support macros:
- A Macro class that groups multiple commands into a single undoable unit
- Executing a macro runs all its commands in order
- Undoing a macro reverses all its commands in reverse order
- A CommandHistory that treats macros as single entries
Demonstrate with a text editor scenario where "Format Paragraph" is a macro of multiple insert/delete operations.
Exercise 15: Adapter for Multiple APIs
You are integrating with three different weather APIs, each with a different interface:
class WeatherAPI_V1:
def get_weather(self, city: str) -> dict:
return {"temp_f": 72, "humidity": 45, "wind_mph": 10}
class WeatherAPI_V2:
def fetch(self, lat: float, lon: float) -> str:
return '{"temperature_c": 22, "humidity_pct": 45, "wind_kmh": 16}'
class WeatherAPI_V3:
def query(self, location: dict) -> list:
return [{"metric": "temp", "value": 295.15, "unit": "K"},
{"metric": "humidity", "value": 45, "unit": "%"},
{"metric": "wind", "value": 4.5, "unit": "m/s"}]
Create a unified WeatherService protocol and write adapters for each API. All adapters should return temperature in Celsius, humidity as a percentage, and wind speed in km/h.
Exercise 16: Code Smell Refactoring Marathon
The following class has at least six code smells. Identify all of them and refactor the class:
class OrderManager:
def __init__(self):
self.orders = []
self.db_host = "localhost"
self.db_port = 5432
self.db_name = "orders"
self.email_host = "smtp.example.com"
self.email_port = 587
self.email_user = "noreply@example.com"
self.tax_rate = 0.08
self.discount_threshold = 100
self.discount_rate = 0.1
def process(self, order_data):
# Validate
if not order_data.get("items"):
return {"error": "No items"}
if not order_data.get("customer_email"):
return {"error": "No email"}
# Calculate
total = 0
for item in order_data["items"]:
total += item["price"] * item["qty"]
# Tax
total_with_tax = total + (total * self.tax_rate)
# Discount
if total_with_tax > self.discount_threshold:
total_with_tax = total_with_tax - (total_with_tax * self.discount_rate)
# Save to database
import psycopg2
conn = psycopg2.connect(host=self.db_host, port=self.db_port, dbname=self.db_name)
cur = conn.cursor()
cur.execute("INSERT INTO orders (email, total) VALUES (%s, %s)",
(order_data["customer_email"], total_with_tax))
conn.commit()
conn.close()
# Send email
import smtplib
from email.mime.text import MIMEText
msg = MIMEText(f"Your order total is ${total_with_tax:.2f}")
msg["Subject"] = "Order Confirmation"
msg["From"] = self.email_user
msg["To"] = order_data["customer_email"]
server = smtplib.SMTP(self.email_host, self.email_port)
server.send_message(msg)
server.quit()
return {"success": True, "total": total_with_tax}
Exercise 17: AI-Specific Patterns
Implement a complete PromptChain class that:
- Takes a list of PromptTemplate objects (from Section 25.9)
- Passes the output of each prompt as input to the next
- Supports branching (different next templates based on the output)
- Includes error handling for each step
- Logs each step's input and output
This implements the "chain of thought" pattern at the application level.
Exercise 18: Pattern Comparison
For the following requirements, implement the solution using two different patterns and compare them. Write 3–5 sentences analyzing the trade-offs.
Requirement: A drawing application supports multiple shapes (circle, rectangle, triangle). Each shape can be drawn, resized, and exported to SVG. New shape types may be added in the future.
Implement using: 1. The Strategy pattern (shape operations as interchangeable strategies) 2. The Template Method pattern (base class with overridable steps)
Tier 4: Synthesis and Design (Exercises 19–24)
Exercise 19: Plugin Architecture
Design and implement a plugin system using Factory and Observer patterns:
- A PluginRegistry (Factory) that discovers and instantiates plugins
- An EventBus (Observer) that plugins use to communicate
- A Plugin protocol defining the lifecycle methods: initialize(), execute(context), shutdown()
- At least three example plugins
- A PluginManager that coordinates the full lifecycle
Write comprehensive docstrings and type hints.
Exercise 20: Mini Framework
Build a minimal web-framework-like request handler using multiple patterns: - Factory for creating request handlers based on URL patterns - Strategy for content negotiation (JSON, XML, HTML responses) - Decorator for middleware (authentication, logging, CORS) - Command for request processing pipeline
The framework should handle at least three different routes.
Exercise 21: AI Conversation System
Build a complete conversation system using the patterns from Section 25.9:
- PromptTemplate for generating prompts
- ConversationManager for managing multi-turn context
- OutputParser for extracting structured data
- RetryWithFallback for resilience
Add a ConversationPersistence layer that can save/load conversations to JSON files.
Exercise 22: Refactoring Challenge
You are given a 100-line monolithic function (provided below in a simplified form). Refactor it into a well-structured system using at least three design patterns. Document each pattern choice and why it fits.
def run_etl_pipeline(source_type, source_config, transformations, destination_type, dest_config):
# Read data
if source_type == "csv":
import csv
with open(source_config["path"]) as f:
data = list(csv.DictReader(f))
elif source_type == "json":
import json
with open(source_config["path"]) as f:
data = json.load(f)
elif source_type == "api":
import requests
resp = requests.get(source_config["url"], headers=source_config.get("headers", {}))
data = resp.json()
# Transform data
for t in transformations:
if t["type"] == "filter":
data = [row for row in data if row.get(t["field"]) == t["value"]]
elif t["type"] == "rename":
data = [{t["new"]: row.pop(t["old"], None), **row} for row in data]
elif t["type"] == "calculate":
for row in data:
row[t["output"]] = eval(t["expression"], {"row": row})
elif t["type"] == "drop_nulls":
data = [row for row in data if all(v is not None for v in row.values())]
# Write data
if destination_type == "csv":
import csv
with open(dest_config["path"], "w", newline="") as f:
writer = csv.DictWriter(f, fieldnames=data[0].keys())
writer.writeheader()
writer.writerows(data)
elif destination_type == "json":
import json
with open(dest_config["path"], "w") as f:
json.dump(data, f, indent=2)
elif destination_type == "database":
pass # database insertion logic
return len(data)
Exercise 23: Pattern Detection in Open Source
Choose a well-known Python open-source project (e.g., Flask, Django, Requests, FastAPI). Identify at least five design patterns used in the project. For each pattern: 1. Name the pattern 2. Locate the specific code (file and approximate line numbers) 3. Explain why the pattern is used in that context 4. Evaluate whether the pattern is the right choice
Write your analysis as a 500-word report.
Exercise 24: Clean Code Metrics
Write a Python script that analyzes a Python source file and reports on clean code metrics:
- Average function length (in lines)
- Maximum function length
- Number of functions with more than 4 parameters
- Percentage of functions with docstrings
- Number of single-letter variable names
- Cyclomatic complexity estimate (count of if, elif, for, while, and, or per function)
Use the ast module for parsing. Test it on your own code from previous exercises.
Tier 5: Expert Challenges (Exercises 25–30)
Exercise 25: Pattern Language
Design a "pattern language" for AI-powered applications. Define at least eight patterns that work together to address common concerns in AI applications: - Input validation and sanitization - Prompt construction - Model selection and fallback - Output parsing and validation - Caching and memoization - Rate limiting - Conversation state management - Error recovery
For each pattern, provide: Name, Intent, Motivation, Structure (class diagram or code), and Example usage. Show how the patterns connect to each other.
Exercise 26: Meta-Pattern Generator
Write a Python metaprogramming tool that generates pattern implementations from a declarative specification:
spec = {
"pattern": "observer",
"subject": "StockMarket",
"events": ["price_changed", "market_opened", "market_closed"],
"observers": [
{"name": "TradingBot", "subscribes_to": ["price_changed"]},
{"name": "Dashboard", "subscribes_to": ["price_changed", "market_opened", "market_closed"]},
{"name": "AlertService", "subscribes_to": ["price_changed"]},
],
}
Given this spec, your tool should generate complete, working Python classes with type hints and docstrings.
Exercise 27: Anti-Pattern Catalog
Create a catalog of at least eight "anti-patterns" specific to AI-assisted development. For each anti-pattern: - Name it creatively (e.g., "Copy-Paste-Pray") - Describe the problem - Explain why AI assistants tend to generate it - Provide a before/after code example - Suggest the correct pattern or principle to apply
Exercise 28: Performance-Aware Patterns
Some patterns introduce overhead (extra function calls, object creation, indirection). Write benchmarks comparing:
1. Strategy as a function vs. Strategy as a class with __call__
2. Observer with list-based dispatch vs. dictionary-based dispatch
3. Factory with dictionary lookup vs. if-elif chain
4. Decorator chain (3 decorators) vs. a single combined function
Use Python's timeit module. Present results and analyze when the overhead matters.
Exercise 29: Domain-Specific Pattern
Choose a specific domain (e.g., e-commerce, healthcare, game development, data pipeline) and design three domain-specific patterns that extend the GoF patterns for that domain. For each: - Define the pattern with a name, intent, and structure - Show a complete Python implementation - Explain how it differs from the base GoF pattern - Provide a realistic usage scenario
Exercise 30: Full Application
Build a complete command-line application (200+ lines) that demonstrates at least six different design patterns working together. The application should be a "Task Automation Engine" that: - Reads task definitions from JSON files (Builder) - Creates task handlers based on type (Factory) - Processes tasks through a pipeline (Command + Chain of Responsibility) - Notifies subscribers on task completion (Observer) - Supports different output formats (Strategy) - Provides a simple high-level API (Facade)
Include comprehensive tests, type hints, and docstrings. The code should pass mypy --strict and ruff check.