What are SOLID Principles
SOLID is a set of five object-oriented design principles proposed by Robert C. Martin (Uncle Bob). They serve as guidelines for designing software with high maintainability, extensibility, and testability.
S - Single Responsibility Principle (SRP)
O - Open/Closed Principle (OCP)
L - Liskov Substitution Principle (LSP)
I - Interface Segregation Principle (ISP)
D - Dependency Inversion Principle (DIP)
1. Single Responsibility Principle (SRP)
A class should have only one responsibility. There should be only one reason to change it.
Violation Example
// Bad example: A class with multiple responsibilities
class User {
constructor(name, email) {
this.name = name;
this.email = email;
}
// Responsibility 1: User data management
getName() { return this.name; }
setName(name) { this.name = name; }
// Responsibility 2: Database operations
save() {
db.query('INSERT INTO users ...');
}
// Responsibility 3: Email sending
sendWelcomeEmail() {
emailService.send(this.email, 'Welcome!');
}
// Responsibility 4: JSON serialization
toJSON() {
return JSON.stringify({ name: this.name, email: this.email });
}
}
Improved Example
// Good example: Separated responsibilities
class User {
constructor(name, email) {
this.name = name;
this.email = email;
}
getName() { return this.name; }
setName(name) { this.name = name; }
}
class UserRepository {
save(user) {
db.query('INSERT INTO users ...');
}
}
class UserNotificationService {
sendWelcomeEmail(user) {
emailService.send(user.email, 'Welcome!');
}
}
class UserSerializer {
toJSON(user) {
return JSON.stringify({ name: user.name, email: user.email });
}
}
2. Open/Closed Principle (OCP)
Software should be open for extension but closed for modification.
Violation Example
// Bad example: Modification required every time a new payment method is added
class PaymentProcessor {
processPayment(type, amount) {
if (type === 'credit') {
// Credit card processing
} else if (type === 'debit') {
// Debit card processing
} else if (type === 'paypal') {
// PayPal processing
}
// Adding a new payment method requires modifying this method
}
}
Improved Example
// Good example: Open for extension
class PaymentProcessor {
constructor(paymentMethod) {
this.paymentMethod = paymentMethod;
}
processPayment(amount) {
return this.paymentMethod.process(amount);
}
}
// Each payment method is an independent class
class CreditCardPayment {
process(amount) { /* Credit card processing */ }
}
class PayPalPayment {
process(amount) { /* PayPal processing */ }
}
// Adding a new payment method without changing existing code
class CryptoPayment {
process(amount) { /* Cryptocurrency processing */ }
}
// Usage
const processor = new PaymentProcessor(new CryptoPayment());
processor.processPayment(1000);
3. Liskov Substitution Principle (LSP)
Derived classes must be substitutable for their base classes.
Violation Example
// Bad example: A square is a type of rectangle, but behaves differently
class Rectangle {
constructor(width, height) {
this.width = width;
this.height = height;
}
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; // Also changes height since it's a square
}
setHeight(height) {
this.width = height; // Also changes width since it's a square
this.height = height;
}
}
// Problematic code
function increaseRectangleWidth(rect) {
rect.setWidth(rect.width + 1);
// For Rectangle: area = (width + 1) * height
// For Square: area = (width + 1) * (width + 1) <- Unexpected result
}
Improved Example
// Good example: Using a common interface
class Shape {
getArea() { throw new Error('Not implemented'); }
}
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; }
}
4. Interface Segregation Principle (ISP)
Clients should not be forced to depend on methods they do not use.
Violation Example
// Bad example: A bloated interface
class Worker {
work() { /* Do work */ }
eat() { /* Eat */ }
sleep() { /* Sleep */ }
}
class Robot extends Worker {
work() { /* Do work */ }
eat() { throw new Error('Robots do not eat'); } // Unnecessary
sleep() { throw new Error('Robots do not sleep'); } // Unnecessary
}
Improved Example
// Good example: Segregated interfaces
class Workable {
work() { throw new Error('Not implemented'); }
}
class Eatable {
eat() { throw new Error('Not implemented'); }
}
class Sleepable {
sleep() { throw new Error('Not implemented'); }
}
class Human {
work() { /* Do work */ }
eat() { /* Eat */ }
sleep() { /* Sleep */ }
}
class Robot {
work() { /* Do work */ }
// eat() and sleep() are not needed
}
5. Dependency Inversion Principle (DIP)
High-level modules should not depend on low-level modules. Both should depend on abstractions.
Violation Example
// Bad example: Directly depending on concrete implementation
class UserService {
constructor() {
this.database = new MySQLDatabase(); // Depends on concrete implementation
}
getUser(id) {
return this.database.query(`SELECT * FROM users WHERE id = ${id}`);
}
}
Improved Example
// Good example: Depending on abstraction (interface)
class Database {
query(sql) { throw new Error('Not implemented'); }
}
class MySQLDatabase extends Database {
query(sql) { /* MySQL implementation */ }
}
class PostgreSQLDatabase extends Database {
query(sql) { /* PostgreSQL implementation */ }
}
class UserService {
constructor(database) {
this.database = database; // Depends on abstraction (dependency injection)
}
getUser(id) {
return this.database.query(`SELECT * FROM users WHERE id = ${id}`);
}
}
// Usage
const userService = new UserService(new PostgreSQLDatabase());
// During testing
const mockDatabase = { query: jest.fn() };
const testService = new UserService(mockDatabase);
Practicing SOLID
Checklist
□ Does the class have only one responsibility? (SRP)
□ Can new features be added without modifying existing code? (OCP)
□ Can derived classes be used in place of base classes? (LSP)
□ Are interfaces kept minimal? (ISP)
□ Are dependencies on abstractions rather than concrete implementations? (DIP)
Balance in Application
Avoid over-application:
- Don't apply complex patterns to simple problems
- Keep YAGNI (You Ain't Gonna Need It) in mind
- Refactor incrementally as needed
Summary
SOLID principles are guidelines for designing software with high maintainability and extensibility. While you don’t need to strictly apply all principles all the time, being mindful of them as design criteria will help you write better code.
← Back to list