Skip to main content
โšก Calmops

SOLID Principles in JavaScript

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

  1. Apply SRP: One responsibility per class:

    // โœ… Good
    class User { }
    class UserRepository { }
    class UserValidator { }
    
    // โŒ Bad
    class User {
      save() { }
      validate() { }
    }
    
  2. Use dependency injection:

    // โœ… Good
    class Service {
      constructor(dependency) {
        this.dependency = dependency;
      }
    }
    
    // โŒ Bad
    class Service {
      constructor() {
        this.dependency = new Dependency();
      }
    }
    
  3. Program to interfaces, not implementations:

    // โœ… Good
    class Service {
      constructor(repository) {
        this.repository = repository;
      }
    }
    
    // โŒ Bad
    class Service {
      constructor() {
        this.repository = new MySQLRepository();
      }
    }
    

Common Mistakes

  1. Violating SRP by mixing concerns:

    // โŒ Bad
    class User {
      save() { }
      sendEmail() { }
      logActivity() { }
    }
    
    // โœ… Good
    class User { }
    class UserRepository { save() { } }
    class EmailService { sendEmail() { } }
    class Logger { logActivity() { } }
    
  2. 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

Next Steps

Comments