SOLID Principles
How it works
- Input arrives
- Core component processes
- Result produced
- Observe and iterate
Learning Objectives
By the end of this topic, you will understand:
- Apply Single Responsibility Principle to create focused, cohesive classes
- Design extensible systems using Open/Closed Principle
- Implement proper inheritance using Liskov Substitution Principle
- Create minimal, focused interfaces following Interface Segregation
- Invert dependencies to achieve flexible, testable architectures
Real-World Examples
Spring Boot: Dependency Inversion through IoC container, enabling flexible configuration and testing
Express.js Middleware: Open/Closed principle allows adding functionality without modifying core framework
TypeScript Interfaces: Interface Segregation in action with focused, role-based type definitions
Clean Architecture: All SOLID principles working together in hexagonal/onion architecture patterns
Overview
SOLID stands for Single Responsibility, Open/Closed, Liskov Substitution, Interface Segregation, and Dependency Inversion.- SRP: A class should have one reason to change.
- OCP: Open for extension, closed for modification.
- LSP: Subtypes must be substitutable for their base types.
- ISP: Prefer many specific interfaces over a single general-purpose one.
- DIP: Depend on abstractions, not concretions.
How it works
- Extract interfaces from concrete classes
- Create adapters for legacy code
- Use dependency injection to wire implementations
- Test via mocks/stubs targeting interfaces
Code example
Applying DIP and OCP with a pluggable Logger dependency injected into a service:interface Logger { log(message: string): void }
class ConsoleLogger implements Logger {
log(message: string): void { console.log('[LOG]', message) }
}
class FileLogger implements Logger {
log(message: string): void { /* append to file */ }
}
class OrderService {
constructor(private logger: Logger) {}
placeOrder(orderId: string): void {
// ... domain logic ...
this.logger.log('Order placed: ' + orderId);
}
}
// Open for extension (swap implementations), closed for modification (no code change in OrderService)
const svc = new OrderService(new ConsoleLogger());
svc.placeOrder('O-1001');
Implementation Notes
- Use dependency injection frameworks to wire dependencies at runtime
- Create abstract base classes or interfaces to define contracts
- Apply Strategy pattern to implement Open/Closed principle effectively
- Use composition root pattern to manage object creation and dependencies
- Implement proper error handling that doesn't violate LSP
Best Practices
- Start with concrete implementations, then extract abstractions as needed
- Keep interfaces focused and role-based rather than feature-based
- Use constructor injection for required dependencies
- Design abstractions based on client needs, not implementation details
- Apply SOLID principles gradually during refactoring sessions
Common Pitfalls
- Over-engineering with unnecessary abstractions and interfaces
- Violating LSP by throwing exceptions in derived classes
- Creating fat interfaces that force clients to depend on unused methods
- Mixing business logic with infrastructure concerns
- Not considering the cost of indirection when applying DIP