SOLID Principles for Programming and Software Design
Introduction
The SOLID principles are a set of guidelines for designing robust, maintainable, and scalable software. They help developers write clean and efficient code by emphasizing separation of concerns and reducing tight coupling between components. These principles are:
- 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)
What is the Single-Responsibility Principle (SRP)?
Definition: A class should have only one reason to change. In other words, a class should have a single responsibility.
Example
// Violates SRP: Handles both user authentication and email notification
class UserService {
authenticateUser(user) {
// Authentication logic
console.log(`${user} authenticated`);
}
sendEmailNotification(user) {
// Email notification logic
console.log(`Email sent to ${user}`);
}
}
// Refactored to follow SRP
class AuthService {
authenticateUser(user) {
console.log(`${user} authenticated`);
}
}
class EmailService {
sendEmailNotification(user) {
console.log(`Email sent to ${user}`);
}
}
By splitting responsibilities into separate classes, we make the code easier to maintain and test.
What is the Open-Closed Principle (OCP)?
Definition: A class should be open for extension but closed for modification. This means that you should be able to add new functionality to a class without changing its existing code.
Example
// Violates OCP: Modification required to add a new payment method
class PaymentProcessor {
processPayment(method) {
if (method === 'creditCard') {
console.log('Processing credit card payment');
} else if (method === 'paypal') {
console.log('Processing PayPal payment');
}
}
}
// Refactored to follow OCP
class PaymentProcessor {
processPayment(paymentMethod) {
paymentMethod.pay();
}
}
class CreditCardPayment {
pay() {
console.log('Processing credit card payment');
}
}
class PayPalPayment {
pay() {
console.log('Processing PayPal payment');
}
}
const processor = new PaymentProcessor();
processor.processPayment(new CreditCardPayment());
processor.processPayment(new PayPalPayment());
Here, new payment methods can be added without modifying the PaymentProcessor
class.
What is the Interface Segregation Principle (ISP)?
Definition: A class should not be forced to implement interfaces it does not use. This principle emphasizes creating smaller, more specific interfaces.
Example
// Violates ISP: Printer class forced to implement unnecessary methods
class Printer {
print() {
console.log('Printing document');
}
scan() {
throw new Error('Scan not supported');
}
}
// Refactored to follow ISP
class PrintService {
print() {
console.log('Printing document');
}
}
class ScanService {
scan() {
console.log('Scanning document');
}
}
const printer = new PrintService();
printer.print();
By separating the responsibilities into smaller interfaces, we avoid unnecessary dependencies.
What is the Dependency Inversion Principle (DIP)?
Definition: 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.
Example
// Violates DIP: High-level class directly depends on low-level class
class BackendDeveloper {
writeBackendCode() {
console.log('Writing backend code');
}
}
class FrontendDeveloper {
writeFrontendCode() {
console.log('Writing frontend code');
}
}
class Project {
constructor() {
this.backendDeveloper = new BackendDeveloper();
this.frontendDeveloper = new FrontendDeveloper();
}
develop() {
this.backendDeveloper.writeBackendCode();
this.frontendDeveloper.writeFrontendCode();
}
}
// Refactored to follow DIP
class Developer {
writeCode() {}
}
class BackendDeveloper extends Developer {
writeCode() {
console.log('Writing backend code');
}
}
class FrontendDeveloper extends Developer {
writeCode() {
console.log('Writing frontend code');
}
}
class Project {
constructor(developers) {
this.developers = developers;
}
develop() {
this.developers.forEach((developer) => developer.writeCode());
}
}
const developers = [new BackendDeveloper(), new FrontendDeveloper()];
const project = new Project(developers);
project.develop();
By relying on abstractions (Developer
), the Project
class is decoupled from specific implementations of developers.