SOLID Principles
SOLID is an acronym for five design principles intended to make object-oriented designs more understandable, flexible, and maintainable. These principles were introduced by Robert C. Martin (Uncle Bob) and are fundamental to writing clean, scalable code.
S
Single Responsibility
A class should have one, and only one, reason to change
O
Open/Closed
Open for extension, closed for modification
L
Liskov Substitution
Subtypes must be substitutable for their base types
I
Interface Segregation
Many specific interfaces are better than one general
D
Dependency Inversion
Depend on abstractions, not concretions
1. Single Responsibility Principle (SRP)
"A class should have one, and only one, reason to change."
Each class should focus on a single task or responsibility. This makes code easier to understand, test, and maintain.
Violation Example
// ❌ VIOLATES SRP - User class has too many responsibilities
class User {
constructor(name, email) {
this.name = name;
this.email = email;
}
// User data responsibility
getName() {
return this.name;
}
// Email validation responsibility
validateEmail() {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(this.email);
}
// Database responsibility
save() {
database.insert('users', this);
}
// Email sending responsibility
sendWelcomeEmail() {
emailService.send(this.email, 'Welcome!');
}
} Following SRP
// ✓ FOLLOWS SRP - Each class has one responsibility
class User {
constructor(name, email) {
this.name = name;
this.email = email;
}
getName() {
return this.name;
}
getEmail() {
return this.email;
}
}
class EmailValidator {
validate(email) {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
}
}
class UserRepository {
save(user) {
database.insert('users', user);
}
}
class EmailService {
sendWelcome(user) {
this.send(user.getEmail(), 'Welcome!');
}
} Benefits:
- Easier to understand and maintain
- Changes to one responsibility don't affect others
- Easier to test in isolation
- Promotes reusability
2. Open/Closed Principle (OCP)
"Software entities should be open for extension, but closed for modification."
You should be able to add new functionality without changing existing code. Use abstraction and polymorphism.
Violation Example
// ❌ VIOLATES OCP - Must modify class to add new discount types
class DiscountCalculator {
calculate(customer, amount) {
if (customer.type === 'regular') {
return amount * 0.1;
} else if (customer.type === 'premium') {
return amount * 0.2;
} else if (customer.type === 'vip') {
return amount * 0.3;
}
// Adding new type requires modifying this class
}
} Following OCP
// ✓ FOLLOWS OCP - Open for extension, closed for modification
class DiscountStrategy {
calculate(amount) {
throw new Error('Must implement calculate method');
}
}
class RegularDiscount extends DiscountStrategy {
calculate(amount) {
return amount * 0.1;
}
}
class PremiumDiscount extends DiscountStrategy {
calculate(amount) {
return amount * 0.2;
}
}
class VIPDiscount extends DiscountStrategy {
calculate(amount) {
return amount * 0.3;
}
}
class Customer {
constructor(discountStrategy) {
this.discountStrategy = discountStrategy;
}
getDiscount(amount) {
return this.discountStrategy.calculate(amount);
}
}
// Easy to add new discount types without modifying existing code Benefits:
- Existing code remains stable when adding features
- Reduces risk of breaking existing functionality
- Promotes use of interfaces and abstraction
- Makes code more maintainable and scalable
3. Liskov Substitution Principle (LSP)
"Objects of a superclass should be replaceable with objects of a subclass without breaking the application."
Subtypes must be substitutable for their base types without altering correctness. Child classes should extend, not break, parent behavior.
Example
// ❌ VIOLATES LSP - Square breaks Rectangle's behavior
class Rectangle {
setWidth(width) {
this.width = width;
}
setHeight(height) {
this.height = height;
}
getArea() {
return this.width * this.height;
}
}
class Square extends Rectangle {
setWidth(width) {
this.width = width;
this.height = width; // Breaks expected behavior!
}
setHeight(height) {
this.width = height;
this.height = height; // Breaks expected behavior!
}
}
// ✓ FOLLOWS LSP - Proper abstraction
class Shape {
getArea() {
throw new Error('Must implement getArea');
}
}
class Rectangle extends Shape {
constructor(width, height) {
super();
this.width = width;
this.height = height;
}
getArea() {
return this.width * this.height;
}
}
class Square extends Shape {
constructor(side) {
super();
this.side = side;
}
getArea() {
return this.side * this.side;
}
} Benefits:
- Ensures inheritance is used correctly
- Prevents unexpected behavior in polymorphism
- Makes code more predictable and reliable
- Supports proper abstraction hierarchies
4. Interface Segregation Principle (ISP)
"No client should be forced to depend on methods it does not use."
Create specific interfaces rather than one general-purpose interface. Classes should only implement methods they need.
Violation Example
// ❌ VIOLATES ISP - Fat interface forces unnecessary implementations
class Worker {
work() {}
eat() {}
sleep() {}
}
class HumanWorker extends Worker {
work() { console.log('Working...'); }
eat() { console.log('Eating...'); }
sleep() { console.log('Sleeping...'); }
}
class RobotWorker extends Worker {
work() { console.log('Working...'); }
eat() { /* Robots don't eat! */ }
sleep() { /* Robots don't sleep! */ }
} Following ISP
// ✓ FOLLOWS ISP - Segregated interfaces
class Workable {
work() {
throw new Error('Must implement work');
}
}
class Eatable {
eat() {
throw new Error('Must implement eat');
}
}
class Sleepable {
sleep() {
throw new Error('Must implement sleep');
}
}
class HumanWorker {
work() { console.log('Working...'); }
eat() { console.log('Eating...'); }
sleep() { console.log('Sleeping...'); }
}
class RobotWorker {
work() { console.log('Working...'); }
// Only implements what it needs!
} Benefits:
- Classes aren't burdened with unnecessary methods
- Changes to interfaces affect fewer classes
- Promotes better separation of concerns
- Makes code more modular and flexible
5. Dependency Inversion Principle (DIP)
"Depend upon abstractions, not concretions."
High-level modules should not depend on low-level modules. Both should depend on abstractions. This is often implemented through dependency injection.
Violation Example
// ❌ VIOLATES DIP - High-level depends on low-level concrete class
class MySQLDatabase {
save(data) {
console.log('Saving to MySQL:', data);
}
}
class UserService {
constructor() {
this.database = new MySQLDatabase(); // Tight coupling!
}
createUser(userData) {
this.database.save(userData);
}
} Following DIP
// ✓ FOLLOWS DIP - Depend on abstractions
class Database {
save(data) {
throw new Error('Must implement save');
}
}
class MySQLDatabase extends Database {
save(data) {
console.log('Saving to MySQL:', data);
}
}
class MongoDBDatabase extends Database {
save(data) {
console.log('Saving to MongoDB:', data);
}
}
class UserService {
constructor(database) {
this.database = database; // Dependency injected!
}
createUser(userData) {
this.database.save(userData);
}
}
// Usage - easy to swap implementations
const mysqlService = new UserService(new MySQLDatabase());
const mongoService = new UserService(new MongoDBDatabase()); Benefits:
- Reduces coupling between modules
- Makes code more testable (easy to mock dependencies)
- Allows swapping implementations easily
- Promotes flexibility and maintainability
Why SOLID Matters
- Maintainability - Easier to update and modify code
- Testability - Simpler to write unit tests
- Flexibility - Easier to adapt to changing requirements
- Scalability - Code grows without becoming unmanageable
- Collaboration - Clearer structure helps teams work together
When to Apply SOLID
SOLID principles are guidelines, not absolute rules. Consider:
- Don't Over-Engineer - Small, simple programs may not need all principles
- Pragmatism - Apply principles when they add value, not dogmatically
- Refactoring - It's okay to violate initially and refactor later as code grows
- Team Standards - Balance between ideal design and team productivity
Resources
- Clean Code - Robert C. Martin (Uncle Bob)
- Agile Software Development - Robert C. Martin
- Design Patterns - Gang of Four (complements SOLID)
- Refactoring - Martin Fowler