Files
freemo d9e5668cec fix(skills): comprehensive final audit pass for programming-patterns skill
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
2026-04-15 13:22:13 -04:00

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();
  • 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.