Fixes and improvements from exhaustive audit: Consistency fixes in SKILL.md: - 'Pipe & Filter' → 'Pipe and Filter' (one stray '&' found and corrected) - 'Singleton for factory instance' → clarified to 'register factory as singleton-scoped via DI container' (less misleading wording) - Documentation Format section updated with note that SKILL.md itself is the authoritative source for related-pattern combinations Coverage fix — Related Patterns sections: - Added '## Related Patterns' to ALL 94 pattern files (was 0/94) - Each section lists 3–6 related patterns with relationship descriptions - Covers: why they're related, when to prefer one vs the other, and which are often confused SOLID principles → Creational → Structural → Behavioral → Architectural → Concurrency → Functional → Resilience → Data Access → Messaging → Testing → Error Handling → Microservice — all 13 categories covered Code verification: - Python: 0 failures (all 85 testable blocks pass) - Go: 0 failures (all 76 testable blocks pass) - JavaScript: 0 failures (all 78 testable blocks pass) - All 239 code blocks verified correct after edits Final skill state: - 108 files, 36,524 lines across 13 reference categories - 94/94 pattern files have Related Patterns sections - 2,815-line SKILL.md with 67 decision trees, 23 scenarios, 0 broken references, 0 naming inconsistencies
11 KiB
Single Responsibility Principle (SRP)
"A class should have only one reason to change." — Robert C. Martin
Problem
A class accumulates multiple responsibilities over time. It handles business logic and persistence and formatting and notification. When any one of those concerns changes, you must modify the class — risking breakage in unrelated behavior. Testing becomes difficult because you cannot exercise one concern without dragging in the others.
Symptoms of SRP violation:
- A class has methods that serve different actors (e.g.,
calculate_pay()for accounting,generate_report()for management) - Changes to logging break business logic
- You cannot unit-test validation without setting up a database connection
- The class file grows endlessly with unrelated methods
Solution
Identify the distinct reasons the class might change. Extract each responsibility into its own class or module. Each resulting unit has a single, well-defined purpose and changes only when that purpose evolves.
When to Use
- A class has grown to serve multiple stakeholders or concerns
- You find yourself modifying one method and worrying about breaking another
- Unit testing requires elaborate setup because concerns are intertwined
- Multiple developers frequently have merge conflicts in the same file
When to Avoid
- The class is genuinely small and cohesive (splitting would just scatter trivial logic)
- You are writing a short script or throwaway prototype
- The "responsibilities" are tightly coupled by nature and splitting them would introduce unnecessary indirection
Pseudocode
Before — mixed responsibilities
CLASS User:
name, email
METHOD validate():
IF name IS EMPTY OR email DOES NOT CONTAIN "@":
RAISE ValidationError
METHOD save_to_database():
DB.execute("INSERT INTO users ...")
METHOD send_welcome_email():
EmailService.send(to=email, subject="Welcome!")
After — each class owns one concern
CLASS User:
name, email
METHOD validate():
IF name IS EMPTY OR email DOES NOT CONTAIN "@":
RAISE ValidationError
CLASS UserRepository:
METHOD save(user):
DB.execute("INSERT INTO users ...")
CLASS UserNotifier:
METHOD send_welcome(user):
EmailService.send(to=user.email, subject="Welcome!")
Now User changes only if validation rules change, UserRepository changes only if the storage mechanism changes, and UserNotifier changes only if notification requirements change.
Python
"""
Single Responsibility Principle — Python
BEFORE: Report class generates content AND formats AND emails.
AFTER: Three focused classes, each with one job.
"""
# === BEFORE: Violating SRP ===
class ReportBefore:
def __init__(self, title: str, data: list[dict]):
self.title = title
self.data = data
def generate_content(self) -> str:
lines = [self.title, "=" * len(self.title)]
for row in self.data:
lines.append(f" {row['item']}: ${row['amount']:.2f}")
total = sum(r["amount"] for r in self.data)
lines.append(f" TOTAL: ${total:.2f}")
return "\n".join(lines)
def print_report(self) -> None:
# Formatting concern mixed in
content = self.generate_content()
border = "#" * 40
print(f"{border}\n{content}\n{border}")
def email_report(self, recipient: str) -> None:
# Notification concern mixed in
content = self.generate_content()
print(f"[EMAIL] Sending to {recipient}:\n{content}")
# === AFTER: Following SRP ===
class Report:
"""Responsible only for holding and generating report content."""
def __init__(self, title: str, data: list[dict]):
self.title = title
self.data = data
def generate_content(self) -> str:
lines = [self.title, "=" * len(self.title)]
for row in self.data:
lines.append(f" {row['item']}: ${row['amount']:.2f}")
total = sum(r["amount"] for r in self.data)
lines.append(f" TOTAL: ${total:.2f}")
return "\n".join(lines)
class ReportPrinter:
"""Responsible only for formatting and printing a report."""
def print_report(self, report: Report) -> None:
content = report.generate_content()
border = "#" * 40
print(f"{border}\n{content}\n{border}")
class ReportEmailer:
"""Responsible only for emailing a report."""
def email_report(self, report: Report, recipient: str) -> None:
content = report.generate_content()
print(f"[EMAIL] Sending to {recipient}:\n{content}")
# === Demo ===
def main():
data = [
{"item": "Widget A", "amount": 29.99},
{"item": "Widget B", "amount": 14.50},
{"item": "Widget C", "amount": 45.00},
]
print("--- BEFORE (SRP violated) ---")
old_report = ReportBefore("Q1 Sales", data)
old_report.print_report()
old_report.email_report("boss@example.com")
print("\n--- AFTER (SRP applied) ---")
report = Report("Q1 Sales", data)
printer = ReportPrinter()
emailer = ReportEmailer()
printer.print_report(report)
emailer.email_report(report, "boss@example.com")
if __name__ == "__main__":
main()
Go
// Single Responsibility Principle — Go
//
// BEFORE: A single UserService struct handles validation, persistence, and notification.
// AFTER: Three separate structs, each owning one concern.
package main
import (
"errors"
"fmt"
"strings"
)
// ============================================================
// BEFORE: Violating SRP — one struct does everything
// ============================================================
type UserServiceBefore struct{}
func (s *UserServiceBefore) Validate(name, email string) error {
if name == "" {
return errors.New("name is required")
}
if !strings.Contains(email, "@") {
return errors.New("invalid email")
}
return nil
}
func (s *UserServiceBefore) Save(name, email string) {
fmt.Printf(" [DB] INSERT INTO users (name, email) VALUES ('%s', '%s')\n", name, email)
}
func (s *UserServiceBefore) SendWelcome(email string) {
fmt.Printf(" [EMAIL] Welcome message sent to %s\n", email)
}
// ============================================================
// AFTER: Following SRP — separate structs per concern
// ============================================================
// User holds only the data.
type User struct {
Name string
Email string
}
// UserValidator is responsible only for validation.
type UserValidator struct{}
func (v *UserValidator) Validate(u User) error {
if u.Name == "" {
return errors.New("name is required")
}
if !strings.Contains(u.Email, "@") {
return errors.New("invalid email")
}
fmt.Printf(" [VALID] User '%s' passed validation\n", u.Name)
return nil
}
// UserRepository is responsible only for persistence.
type UserRepository struct{}
func (r *UserRepository) Save(u User) {
fmt.Printf(" [DB] INSERT INTO users (name, email) VALUES ('%s', '%s')\n", u.Name, u.Email)
}
// UserNotifier is responsible only for notifications.
type UserNotifier struct{}
func (n *UserNotifier) SendWelcome(u User) {
fmt.Printf(" [EMAIL] Welcome message sent to %s\n", u.Email)
}
// ============================================================
// Demo
// ============================================================
func main() {
fmt.Println("--- BEFORE (SRP violated) ---")
svc := &UserServiceBefore{}
if err := svc.Validate("Alice", "alice@example.com"); err != nil {
fmt.Println(" Error:", err)
} else {
svc.Save("Alice", "alice@example.com")
svc.SendWelcome("alice@example.com")
}
fmt.Println("\n--- AFTER (SRP applied) ---")
user := User{Name: "Alice", Email: "alice@example.com"}
validator := &UserValidator{}
repo := &UserRepository{}
notifier := &UserNotifier{}
if err := validator.Validate(user); err != nil {
fmt.Println(" Error:", err)
return
}
repo.Save(user)
notifier.SendWelcome(user)
}
JavaScript
// Single Responsibility Principle — JavaScript
//
// BEFORE: OrderProcessor handles price calculation, persistence, AND receipt formatting.
// AFTER: Split into OrderCalculator, OrderRepository, and ReceiptFormatter.
// ============================================================
// BEFORE: Violating SRP
// ============================================================
class OrderProcessorBefore {
constructor(items) {
this.items = items;
}
calculateTotal() {
return this.items.reduce((sum, item) => sum + item.price * item.qty, 0);
}
saveToDatabase() {
const total = this.calculateTotal();
console.log(` [DB] Saved order — ${this.items.length} items, total: $${total.toFixed(2)}`);
}
printReceipt() {
console.log(" --- Receipt ---");
for (const item of this.items) {
console.log(` ${item.name} x${item.qty} = $${(item.price * item.qty).toFixed(2)}`);
}
console.log(` TOTAL: $${this.calculateTotal().toFixed(2)}`);
console.log(" ----------------");
}
}
// ============================================================
// AFTER: Following SRP
// ============================================================
class Order {
constructor(items) {
this.items = items;
}
}
class OrderCalculator {
/** Responsible only for price computation. */
calculateTotal(order) {
return order.items.reduce((sum, item) => sum + item.price * item.qty, 0);
}
}
class OrderRepository {
/** Responsible only for persistence. */
save(order, total) {
console.log(` [DB] Saved order — ${order.items.length} items, total: $${total.toFixed(2)}`);
}
}
class ReceiptFormatter {
/** Responsible only for formatting and printing receipts. */
print(order, total) {
console.log(" --- Receipt ---");
for (const item of order.items) {
console.log(` ${item.name} x${item.qty} = $${(item.price * item.qty).toFixed(2)}`);
}
console.log(` TOTAL: $${total.toFixed(2)}`);
console.log(" ----------------");
}
}
// ============================================================
// Demo
// ============================================================
function main() {
const items = [
{ name: "Keyboard", price: 49.99, qty: 1 },
{ name: "Mouse", price: 29.99, qty: 2 },
{ name: "USB Cable", price: 9.99, qty: 3 },
];
console.log("--- BEFORE (SRP violated) ---");
const oldProcessor = new OrderProcessorBefore(items);
oldProcessor.saveToDatabase();
oldProcessor.printReceipt();
console.log("\n--- AFTER (SRP applied) ---");
const order = new Order(items);
const calculator = new OrderCalculator();
const repository = new OrderRepository();
const formatter = new ReceiptFormatter();
const total = calculator.calculateTotal(order);
repository.save(order, total);
formatter.print(order, total);
}
main();
Related Patterns
- OCP — once responsibilities are separated (SRP), each class can be extended independently (OCP).
- Facade — after applying SRP, a facade can re-unify the separated classes behind a simple interface.
- Service Layer — the service layer applies SRP at the architectural level: orchestration logic is separated from domain logic and persistence.
- Mediator — a mediator centralises coordination logic so individual components each have a single responsibility.