SOLID Principles in JavaScript
SOLID principles are fundamental design guidelines for writing maintainable, scalable code. This article covers all five principles with practical JavaScript examples.
Introduction
SOLID principles provide:
- Code maintainability
- Scalability
- Flexibility
- Testability
- Reduced coupling
Understanding SOLID helps you:
- Write better code
- Design flexible systems
- Reduce technical debt
- Improve collaboration
- Build sustainable projects
Single Responsibility Principle (SRP)
Definition
A class should have only one reason to change. Each class should have a single responsibility.
Bad Example
// โ Bad: Multiple responsibilities
class User {
constructor(name, email) {
this.name = name;
this.email = email;
}
// Responsibility 1: User data
getUser() {
return { name: this.name, email: this.email };
}
// Responsibility 2: Database operations
saveToDatabase() {
// Save to database
}
// Responsibility 3: Email sending
sendWelcomeEmail() {
// Send email
}
// Responsibility 4: Logging
logUserCreation() {
console.log(`User ${this.name} created`);
}
}
Good Example
// โ
Good: Single responsibility per class
class User {
constructor(name, email) {
this.name = name;
this.email = email;
}
getUser() {
return { name: this.name, email: this.email };
}
}
class UserRepository {
save(user) {
// Save to database
}
findById(id) {
// Find user by ID
}
}
class EmailService {
sendWelcomeEmail(user) {
// Send welcome email
}
}
class Logger {
logUserCreation(user) {
console.log(`User ${user.name} created`);
}
}
// Usage
const user = new User('John', '[email protected]');
const repository = new UserRepository();
const emailService = new EmailService();
const logger = new Logger();
repository.save(user);
emailService.sendWelcomeEmail(user);
logger.logUserCreation(user);
Open/Closed Principle (OCP)
Definition
Software entities should be open for extension but closed for modification.
Bad Example
// โ Bad: Requires modification to extend
class PaymentProcessor {
processPayment(payment) {
if (payment.type === 'credit_card') {
// Process credit card
} else if (payment.type === 'paypal') {
// Process PayPal
} else if (payment.type === 'bitcoin') {
// Process Bitcoin
}
// Adding new payment type requires modifying this class
}
}
Good Example
// โ
Good: Open for extension, closed for modification
class PaymentProcessor {
constructor(strategy) {
this.strategy = strategy;
}
processPayment(amount) {
return this.strategy.process(amount);
}
}
class CreditCardPayment {
process(amount) {
console.log(`Processing credit card payment: $${amount}`);
return { success: true, amount };
}
}
class PayPalPayment {
process(amount) {
console.log(`Processing PayPal payment: $${amount}`);
return { success: true, amount };
}
}
class BitcoinPayment {
process(amount) {
console.log(`Processing Bitcoin payment: ${amount} BTC`);
return { success: true, amount };
}
}
// Usage
const creditCardProcessor = new PaymentProcessor(new CreditCardPayment());
creditCardProcessor.processPayment(100);
const paypalProcessor = new PaymentProcessor(new PayPalPayment());
paypalProcessor.processPayment(100);
// Adding new payment type doesn't require modifying PaymentProcessor
class ApplePayPayment {
process(amount) {
console.log(`Processing Apple Pay payment: $${amount}`);
return { success: true, amount };
}
}
const applePayProcessor = new PaymentProcessor(new ApplePayPayment());
applePayProcessor.processPayment(100);
Liskov Substitution Principle (LSP)
Definition
Objects of a superclass should be replaceable with objects of its subclasses without breaking the application.
Bad Example
// โ Bad: Violates LSP
class Bird {
fly() {
return 'Flying';
}
}
class Penguin extends Bird {
fly() {
throw new Error('Penguins cannot fly');
}
}
// This breaks LSP because Penguin can't be used where Bird is expected
function makeBirdFly(bird) {
return bird.fly(); // Fails for Penguin
}
Good Example
// โ
Good: Respects LSP
class Bird {
move() {
// Base movement
}
}
class FlyingBird extends Bird {
move() {
return 'Flying';
}
}
class SwimmingBird extends Bird {
move() {
return 'Swimming';
}
}
class Penguin extends SwimmingBird {
move() {
return 'Swimming and waddling';
}
}
class Eagle extends FlyingBird {
move() {
return 'Flying high';
}
}
// Now any Bird can be used interchangeably
function makeBirdMove(bird) {
return bird.move(); // Works for all birds
}
console.log(makeBirdMove(new Penguin())); // 'Swimming and waddling'
console.log(makeBirdMove(new Eagle())); // 'Flying high'
Interface Segregation Principle (ISP)
Definition
Clients should not be forced to depend on interfaces they don’t use.
Bad Example
// โ Bad: Fat interface
class Worker {
work() { }
eat() { }
sleep() { }
}
class Robot extends Worker {
work() {
return 'Working';
}
eat() {
throw new Error('Robots do not eat');
}
sleep() {
throw new Error('Robots do not sleep');
}
}
// Robot is forced to implement methods it doesn't need
Good Example
// โ
Good: Segregated interfaces
class Workable {
work() { }
}
class Eatable {
eat() { }
}
class Sleepable {
sleep() { }
}
class Human extends Workable {
work() {
return 'Working';
}
}
Object.assign(Human.prototype, new Eatable(), new Sleepable());
class Robot extends Workable {
work() {
return 'Working';
}
}
// Robot only implements what it needs
const human = new Human();
console.log(human.work()); // 'Working'
const robot = new Robot();
console.log(robot.work()); // 'Working'
Dependency Inversion Principle (DIP)
Definition
High-level modules should not depend on low-level modules. Both should depend on abstractions.
Bad Example
// โ Bad: High-level depends on low-level
class MySQLDatabase {
save(data) {
console.log('Saving to MySQL:', data);
}
}
class UserService {
constructor() {
this.database = new MySQLDatabase(); // Direct dependency
}
createUser(user) {
this.database.save(user);
}
}
// Changing database requires modifying UserService
Good Example
// โ
Good: Both depend on abstraction
class UserService {
constructor(database) {
this.database = database; // Injected dependency
}
createUser(user) {
this.database.save(user);
}
}
class MySQLDatabase {
save(data) {
console.log('Saving to MySQL:', data);
}
}
class MongoDBDatabase {
save(data) {
console.log('Saving to MongoDB:', data);
}
}
// Usage
const mysqlDb = new MySQLDatabase();
const userService1 = new UserService(mysqlDb);
userService1.createUser({ name: 'John' });
const mongoDb = new MongoDBDatabase();
const userService2 = new UserService(mongoDb);
userService2.createUser({ name: 'Jane' });
// Easy to switch databases without modifying UserService
Practical SOLID Examples
E-commerce System
// โ
Good: SOLID e-commerce system
// SRP: Each class has one responsibility
class Product {
constructor(id, name, price) {
this.id = id;
this.name = name;
this.price = price;
}
}
class ShoppingCart {
constructor() {
this.items = [];
}
addItem(product) {
this.items.push(product);
}
getTotal() {
return this.items.reduce((sum, item) => sum + item.price, 0);
}
}
// OCP: Open for extension
class PaymentStrategy {
process(amount) {
throw new Error('Must implement process method');
}
}
class CreditCardPayment extends PaymentStrategy {
process(amount) {
console.log(`Processing credit card: $${amount}`);
return true;
}
}
class PayPalPayment extends PaymentStrategy {
process(amount) {
console.log(`Processing PayPal: $${amount}`);
return true;
}
}
// DIP: Depends on abstraction
class Order {
constructor(cart, paymentStrategy) {
this.cart = cart;
this.paymentStrategy = paymentStrategy;
}
checkout() {
const total = this.cart.getTotal();
return this.paymentStrategy.process(total);
}
}
// Usage
const cart = new ShoppingCart();
cart.addItem(new Product(1, 'Laptop', 999));
cart.addItem(new Product(2, 'Mouse', 29));
const payment = new CreditCardPayment();
const order = new Order(cart, payment);
order.checkout(); // Processing credit card: $1028
Notification System
// โ
Good: SOLID notification system
// SRP: Each notifier has one responsibility
class EmailNotifier {
send(recipient, message) {
console.log(`Email to ${recipient}: ${message}`);
}
}
class SMSNotifier {
send(recipient, message) {
console.log(`SMS to ${recipient}: ${message}`);
}
}
class SlackNotifier {
send(recipient, message) {
console.log(`Slack to ${recipient}: ${message}`);
}
}
// ISP: Segregated interfaces
class NotificationService {
constructor(notifiers = []) {
this.notifiers = notifiers;
}
addNotifier(notifier) {
this.notifiers.push(notifier);
}
notify(recipient, message) {
this.notifiers.forEach(notifier => {
notifier.send(recipient, message);
});
}
}
// Usage
const service = new NotificationService();
service.addNotifier(new EmailNotifier());
service.addNotifier(new SMSNotifier());
service.addNotifier(new SlackNotifier());
service.notify('[email protected]', 'Hello!');
// Email to [email protected]: Hello!
// SMS to [email protected]: Hello!
// Slack to [email protected]: Hello!
Best Practices
-
Apply SRP: One responsibility per class:
// โ Good class User { } class UserRepository { } class UserValidator { } // โ Bad class User { save() { } validate() { } } -
Use dependency injection:
// โ Good class Service { constructor(dependency) { this.dependency = dependency; } } // โ Bad class Service { constructor() { this.dependency = new Dependency(); } } -
Program to interfaces, not implementations:
// โ Good class Service { constructor(repository) { this.repository = repository; } } // โ Bad class Service { constructor() { this.repository = new MySQLRepository(); } }
Common Mistakes
-
Violating SRP by mixing concerns:
// โ Bad class User { save() { } sendEmail() { } logActivity() { } } // โ Good class User { } class UserRepository { save() { } } class EmailService { sendEmail() { } } class Logger { logActivity() { } } -
Tight coupling:
// โ Bad class Service { constructor() { this.db = new Database(); } } // โ Good class Service { constructor(db) { this.db = db; } }
Summary
SOLID principles guide sustainable code design. Key takeaways:
- Single Responsibility: One reason to change
- Open/Closed: Extend without modifying
- Liskov Substitution: Substitutable implementations
- Interface Segregation: Specific interfaces
- Dependency Inversion: Depend on abstractions
- Improves maintainability
- Reduces coupling
- Enables testing
Related Resources
- SOLID Principles - Wikipedia
- SOLID Design Patterns - Refactoring Guru
- Dependency Injection - MDN
- Design Patterns in JavaScript
- Clean Code - Robert C. Martin
Next Steps
- Learn about Creational Design Patterns
- Explore Structural Design Patterns
- Study Behavioral Design Patterns
- Practice SOLID principles
- Refactor existing code
Comments