Object-Oriented Design & Design Patterns Master Guide: SOLID and Production Patterns
Object-Oriented Programming (OOP) is the cornerstone of modern software engineering. However, simply declaring classes and using basic inheritance can lead to rigid, fragile, and highly coupled codebases that break upon every new business requirement.
To build sustainable software, we must adhere to SOLID principles, prioritize composition over inheritance, and apply key structural design patterns. This guide provides code-level examples of SOLID architecture and four essential design patterns: Strategy, Factory, Observer, and Command.
1. SOLID Design Principles: 5 Rules for Resilient Code
SOLID principles reduce coupling and increase cohesion, limiting side effects when code is modified.
- Single Responsibility Principle (SRP): A class should have only one reason to change. Separate business entities from their storage (
Repository) and formatting (Serializer) layers. - Open-Closed Principle (OCP): Software entities should be open for extension but closed for modification. Implement this by relying on interfaces to dynamically extend behavior.
- Liskov Substitution Principle (LSP): Subtypes must be substitutable for their base types without altering correctness. Child classes should never narrow or break the behavior contract of the parent.
- Interface Segregation Principle (ISP): Clients should not be forced to depend on methods they do not use. Split broad, generic interfaces into smaller, role-specific contracts.
- Dependency Inversion Principle (DIP): High-level modules should not depend on low-level modules; both should depend on abstractions. This is the foundation of Dependency Injection frameworks.
2. Choosing Composition Over Inheritance
A common mistake in OOP is using inheritance as a primary tool for code reuse. Inheritance creates a rigid compile-time Is-A relationship. Changes in parent class internals ripple down and break subclass behavior.
Conversely, Composition models a Has-A relationship. Instead of inheriting from a class, incorporate an interface reference as a member field and delegate actions to it at runtime.
- Benefits of Composition: You can swap delegate instances dynamically at runtime, work around single-inheritance language limits, and contain structural changes within individual classes.
3. Four Core Design Patterns
1) Strategy Pattern
Enables dynamic swapping of algorithms (e.g., swapping payment processors or discount calculations) at runtime.
- TypeScript Example:
interface PaymentStrategy { pay(amount: number): void; } class CardPayment implements PaymentStrategy { pay(amount: number) { /* Card Logic */ } } class TossPayment implements PaymentStrategy { pay(amount: number) { /* Toss Logic */ } } class PaymentProcessor { constructor(private strategy: PaymentStrategy) {} execute(amount: number) { this.strategy.pay(amount); } }
2) Factory Pattern
Decouples client classes from direct instantiation logic by encapsulating object creation within a factory method or class.
- TypeScript Example:
interface Document { open(): void; } class PdfDocument implements Document { open() { console.log("PDF"); } } class ExcelDocument implements Document { open() { console.log("Excel"); } } class DocumentFactory { static createDocument(type: string): Document { if (type === "pdf") return new PdfDocument(); if (type === "excel") return new ExcelDocument(); throw new Error("Unknown type"); } }
3) Observer Pattern
Establishes a one-to-many relationship where state changes in a publisher trigger automated notifications to subscribed listener instances.
- TypeScript Example:
interface Observer { update(state: string): void; } class AlertSystem implements Observer { update(state: string) { console.log(`Alert: ${state}`); } } class Subject { private observers: Observer[] = []; attach(o: Observer) { this.observers.push(o); } notify(state: string) { this.observers.forEach(o => o.update(state)); } }
4) Command Pattern
Encapsulates an operation as an independent command object, enabling request queueing, logging, and undo/redo operations.
- TypeScript Example:
interface Command { execute(): void; undo(): void; } class LightOnCommand implements Command { constructor(private light: Light) {} execute() { this.light.turnOn(); } undo() { this.light.turnOff(); } }
4. Identifying Architecture Bottlenecks and Code Smells
Refactoring targets should be guided by recognizing common structural symptoms:
- God Classes: A single class containing thousands of lines of code handling data mapping, networking, database connections, and business logic. Fix: Apply SRP and extract distinct concerns into separate, smaller classes.
- Shotgun Surgery: A code smell where making a single modification (like altering a data field) requires making edits across dozens of separate files. Fix: Encapsulate related procedures within a single module and interact with it through a strict API.
Start Here
Continue with the core guides that pull steady search traffic.
- Middleware Troubleshooting Master Guide: Redis, RabbitMQ, and Kafka Operations A comprehensive operations guide to diagnosing Redis big keys and OOM errors, configuring RabbitMQ DLX and Quorum Queues, and resolving Kafka producer retries and leader imbalance issues.
- Vercel Deployment & Troubleshooting Master Guide: Build Errors, Domains, and Tuning A frontend deployment handbook covering Astro Vercel adapters, fixing build-time OOM errors, resolving 504 Gateway Timeouts, and managing custom domains and rollbacks.
- Practical AI Agent & LLM Application Development Master Guide A comprehensive guide to building agentic workflows, adjusting temperature and sampling, managing context windows, optimizing latency, implementing RAG and vector search, and managing multi-agent graphs.
Related guides are shown to help you explore more.