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
14 KiB
Dependency Inversion Principle (DIP)
"High-level modules should not depend on low-level modules. Both should depend on abstractions. Abstractions should not depend on details. Details should depend on abstractions." — Robert C. Martin
Problem
A high-level module (business logic, orchestration) directly imports and instantiates a low-level module (database driver, email SDK, file system). This means:
- Tight coupling — changing the database from PostgreSQL to MongoDB forces changes in business logic
- Untestable — you cannot unit-test the business logic without a live database
- Rigid — swapping implementations (e.g., email provider) requires editing the high-level code
- Ripple effects — low-level implementation details leak into the architecture
Classic smell: Business logic classes contain import psycopg2, new MySQLConnection(), or require('nodemailer') directly.
Solution
Introduce an abstraction (interface, abstract class, protocol) that the high-level module depends on. The low-level module implements that abstraction. The high-level module receives its dependency via injection (constructor, parameter, or factory) rather than creating it internally.
The "inversion" is that the dependency arrow flips: instead of HighLevel → LowLevel, you get HighLevel → Abstraction ← LowLevel.
When to Use
- Business logic directly instantiates infrastructure (DB, HTTP, file I/O, queues)
- You want to unit-test high-level code without infrastructure
- You need to swap implementations (e.g., in-memory cache vs. Redis) without changing callers
- You are building a system where different deployments use different backends
When to Avoid
- The low-level module is trivially simple and will never be swapped (e.g.,
math.sqrt) - You are writing a small script with no tests and no deployment variants
- Introducing an abstraction would add complexity without any foreseeable benefit
Pseudocode
Before — high-level depends directly on low-level
CLASS EmailSender:
METHOD send(to, subject, body):
// Talks to SMTP server directly
SMTP.connect("mail.example.com")
SMTP.send(to, subject, body)
CLASS NotificationService:
email_sender = NEW EmailSender() // Direct dependency!
METHOD notify(user, message):
email_sender.send(user.email, "Alert", message)
After — both depend on an abstraction
INTERFACE MessageSender:
METHOD send(to, subject, body)
CLASS EmailSender IMPLEMENTS MessageSender:
METHOD send(to, subject, body):
SMTP.connect("mail.example.com")
SMTP.send(to, subject, body)
CLASS SMSSender IMPLEMENTS MessageSender:
METHOD send(to, subject, body):
SMS_API.send(to, body)
CLASS NotificationService:
sender: MessageSender // Depends on abstraction
CONSTRUCTOR(sender: MessageSender):
this.sender = sender // Injected!
METHOD notify(user, message):
sender.send(user.contact, "Alert", message)
// Wiring (at the application edge):
service = NEW NotificationService(NEW EmailSender())
// or
service = NEW NotificationService(NEW SMSSender())
Python
"""
Dependency Inversion Principle — Python
BEFORE: NotificationService directly creates an EmailSender (tight coupling).
AFTER: NotificationService depends on a MessageSender ABC; implementation is injected.
"""
from abc import ABC, abstractmethod
# ============================================================
# BEFORE: Violating DIP — direct dependency on a concrete class
# ============================================================
class EmailSenderBefore:
def send(self, to: str, subject: str, body: str) -> None:
print(f" [SMTP] Sending email to {to}: '{subject}' — {body}")
class NotificationServiceBefore:
def __init__(self):
# Hard-coded dependency — cannot swap, cannot test in isolation
self.sender = EmailSenderBefore()
def notify(self, user_email: str, message: str) -> None:
self.sender.send(user_email, "Alert", message)
# ============================================================
# AFTER: Following DIP — depend on abstraction, inject implementation
# ============================================================
class MessageSender(ABC):
"""Abstraction that both high-level and low-level code depend on."""
@abstractmethod
def send(self, to: str, subject: str, body: str) -> None: ...
class EmailSender(MessageSender):
def send(self, to: str, subject: str, body: str) -> None:
print(f" [SMTP] Email to {to}: '{subject}' — {body}")
class SMSSender(MessageSender):
def send(self, to: str, subject: str, body: str) -> None:
print(f" [SMS] Text to {to}: {body}")
class ConsoleSender(MessageSender):
"""Useful for testing — no real I/O."""
def send(self, to: str, subject: str, body: str) -> None:
print(f" [CONSOLE] To={to}, Subject='{subject}', Body='{body}'")
class NotificationService:
"""High-level module — depends on MessageSender abstraction, not a concrete class."""
def __init__(self, sender: MessageSender):
self.sender = sender # Injected!
def notify(self, recipient: str, message: str) -> None:
self.sender.send(recipient, "Alert", message)
# ============================================================
# Demo
# ============================================================
def main():
print("--- BEFORE (DIP violated) ---")
old_service = NotificationServiceBefore()
old_service.notify("alice@example.com", "Server is down!")
print("\n--- AFTER (DIP applied) ---")
print("Using EmailSender:")
email_service = NotificationService(EmailSender())
email_service.notify("alice@example.com", "Server is down!")
print("\nUsing SMSSender:")
sms_service = NotificationService(SMSSender())
sms_service.notify("+1-555-0199", "Server is down!")
print("\nUsing ConsoleSender (for tests):")
test_service = NotificationService(ConsoleSender())
test_service.notify("test-user", "Server is down!")
if __name__ == "__main__":
main()
Go
// Dependency Inversion Principle — Go
//
// BEFORE: OrderService directly creates a PostgresDB (tight coupling).
// AFTER: OrderService depends on a Repository interface; implementation is injected.
package main
import "fmt"
// ============================================================
// BEFORE: Violating DIP — concrete dependency
// ============================================================
type PostgresDBBefore struct{}
func (db *PostgresDBBefore) SaveOrder(id string, total float64) {
fmt.Printf(" [POSTGRES] INSERT INTO orders (id, total) VALUES ('%s', %.2f)\n", id, total)
}
func (db *PostgresDBBefore) FindOrder(id string) string {
return fmt.Sprintf("[POSTGRES] Order %s found", id)
}
type OrderServiceBefore struct {
db *PostgresDBBefore // Direct dependency on concrete type!
}
func NewOrderServiceBefore() *OrderServiceBefore {
return &OrderServiceBefore{db: &PostgresDBBefore{}}
}
func (s *OrderServiceBefore) PlaceOrder(id string, total float64) {
fmt.Printf(" Placing order %s...\n", id)
s.db.SaveOrder(id, total)
}
func (s *OrderServiceBefore) GetOrder(id string) {
result := s.db.FindOrder(id)
fmt.Printf(" %s\n", result)
}
// ============================================================
// AFTER: Following DIP — depend on interface, inject implementation
// ============================================================
// Repository is the abstraction both high-level and low-level depend on.
type Repository interface {
Save(id string, total float64)
Find(id string) string
}
// PostgresRepository implements Repository.
type PostgresRepository struct{}
func (r *PostgresRepository) Save(id string, total float64) {
fmt.Printf(" [POSTGRES] INSERT INTO orders (id, total) VALUES ('%s', %.2f)\n", id, total)
}
func (r *PostgresRepository) Find(id string) string {
return fmt.Sprintf("[POSTGRES] Order %s found", id)
}
// InMemoryRepository implements Repository — great for tests.
type InMemoryRepository struct {
store map[string]float64
}
func NewInMemoryRepository() *InMemoryRepository {
return &InMemoryRepository{store: make(map[string]float64)}
}
func (r *InMemoryRepository) Save(id string, total float64) {
r.store[id] = total
fmt.Printf(" [MEMORY] Stored order %s = %.2f\n", id, total)
}
func (r *InMemoryRepository) Find(id string) string {
if total, ok := r.store[id]; ok {
return fmt.Sprintf("[MEMORY] Order %s = %.2f", id, total)
}
return fmt.Sprintf("[MEMORY] Order %s not found", id)
}
// OrderService depends on the Repository interface — not a concrete DB.
type OrderService struct {
repo Repository // Injected!
}
func NewOrderService(repo Repository) *OrderService {
return &OrderService{repo: repo}
}
func (s *OrderService) PlaceOrder(id string, total float64) {
fmt.Printf(" Placing order %s...\n", id)
s.repo.Save(id, total)
}
func (s *OrderService) GetOrder(id string) {
result := s.repo.Find(id)
fmt.Printf(" %s\n", result)
}
// ============================================================
// Demo
// ============================================================
func main() {
fmt.Println("--- BEFORE (DIP violated) ---")
oldService := NewOrderServiceBefore()
oldService.PlaceOrder("ORD-001", 99.99)
oldService.GetOrder("ORD-001")
fmt.Println("\n--- AFTER (DIP applied) ---")
fmt.Println("Using PostgresRepository:")
pgService := NewOrderService(&PostgresRepository{})
pgService.PlaceOrder("ORD-001", 99.99)
pgService.GetOrder("ORD-001")
fmt.Println("\nUsing InMemoryRepository (for tests):")
memService := NewOrderService(NewInMemoryRepository())
memService.PlaceOrder("ORD-002", 149.50)
memService.GetOrder("ORD-002")
memService.GetOrder("ORD-999") // Not found
}
JavaScript
// Dependency Inversion Principle — JavaScript
//
// BEFORE: UserService directly instantiates MySQLDatabase (tight coupling).
// AFTER: UserService receives a database abstraction via constructor injection.
// ============================================================
// BEFORE: Violating DIP — concrete dependency baked in
// ============================================================
class MySQLDatabaseBefore {
save(table, data) {
console.log(` [MYSQL] INSERT INTO ${table} SET ${JSON.stringify(data)}`);
}
findById(table, id) {
console.log(` [MYSQL] SELECT * FROM ${table} WHERE id = ${id}`);
return { id, name: "Alice", email: "alice@example.com" };
}
}
class UserServiceBefore {
constructor() {
// Hard-coded dependency — cannot swap, cannot test easily
this.db = new MySQLDatabaseBefore();
}
createUser(name, email) {
console.log(` Creating user ${name}...`);
this.db.save("users", { name, email });
}
getUser(id) {
const user = this.db.findById("users", id);
console.log(` Found: ${JSON.stringify(user)}`);
}
}
// ============================================================
// AFTER: Following DIP — depend on abstraction, inject implementation
// ============================================================
/**
* Database "interface" (enforced by convention in JS).
* Any implementation must provide save() and findById().
*/
class MySQLDatabase {
save(table, data) {
console.log(` [MYSQL] INSERT INTO ${table} SET ${JSON.stringify(data)}`);
}
findById(table, id) {
console.log(` [MYSQL] SELECT * FROM ${table} WHERE id = ${id}`);
return { id, name: "Alice", email: "alice@example.com" };
}
}
class InMemoryDatabase {
constructor() {
this.store = new Map();
this.nextId = 1;
}
save(table, data) {
const id = this.nextId++;
const key = `${table}:${id}`;
const record = { id, ...data };
this.store.set(key, record);
console.log(` [MEMORY] Stored ${key} = ${JSON.stringify(record)}`);
}
findById(table, id) {
const key = `${table}:${id}`;
const record = this.store.get(key) || null;
if (record) {
console.log(` [MEMORY] Found ${key} = ${JSON.stringify(record)}`);
} else {
console.log(` [MEMORY] ${key} not found`);
}
return record;
}
}
class UserService {
/**
* Depends on a database abstraction — not a specific implementation.
* @param {{ save: Function, findById: Function }} database
*/
constructor(database) {
this.db = database; // Injected!
}
createUser(name, email) {
console.log(` Creating user ${name}...`);
this.db.save("users", { name, email });
}
getUser(id) {
const user = this.db.findById("users", id);
if (user) {
console.log(` Result: ${JSON.stringify(user)}`);
}
}
}
// ============================================================
// Demo
// ============================================================
function main() {
console.log("--- BEFORE (DIP violated) ---");
const oldService = new UserServiceBefore();
oldService.createUser("Alice", "alice@example.com");
oldService.getUser(1);
console.log("\n--- AFTER (DIP applied) ---");
console.log("Using MySQLDatabase:");
const mysqlService = new UserService(new MySQLDatabase());
mysqlService.createUser("Alice", "alice@example.com");
mysqlService.getUser(1);
console.log("\nUsing InMemoryDatabase (for tests):");
const memService = new UserService(new InMemoryDatabase());
memService.createUser("Bob", "bob@example.com");
memService.createUser("Carol", "carol@example.com");
memService.getUser(1);
memService.getUser(2);
memService.getUser(99); // Not found
}
main();
Related Patterns
- Dependency Injection — DI is the practical mechanism for implementing DIP: abstractions are injected rather than created internally.
- Repository — the repository pattern applies DIP at the data layer: business logic depends on a repository interface, not a specific database driver.
- Adapter — adapters implement the abstraction defined by the high-level module, making low-level details conform to the required interface.
- Hexagonal Architecture — hexagonal architecture (Ports & Adapters) is an architectural style built entirely on DIP.