DEV Community

Chris
Chris

Posted on

Mastering SOLID Principles: Write Clean & Maintainable Code πŸš€

As a developer, writing clean and maintainable code is crucial. The SOLID principles help us achieve that by improving code structure, making it more flexible and easier to manage.

In this post, we’ll break down each SOLID principle with detailed explanations and real-world examples in JavaScript/TypeScript, so that anyone can understand and apply them effectively! πŸ› οΈ


🟠 S - Single Responsibility Principle (SRP)

Definition

"A class should have only one reason to change."

The Single Responsibility Principle (SRP) states that every class, module, or function should have a single, well-defined purpose. This makes the codebase more maintainable, as changes to one functionality won’t unintentionally impact another.

Why is SRP important?

  • Reduces the complexity of a class by focusing on one responsibility.
  • Makes code easier to debug, test, and modify.
  • Improves code reusability by separating concerns.

❌ Bad Example:

class User {
  constructor(private name: string, private email: string) {}

  saveToDatabase() {
    console.log(`Saving ${this.name} to database...`);
  }
}
Enter fullscreen mode Exit fullscreen mode

πŸ”΄ Here, the User class is responsible for both storing user data and interacting with the database. If we change how data is stored (e.g., switch from SQL to NoSQL), we must modify this class.

βœ… Good Example:

class User {
  constructor(private name: string, private email: string) {}
}

class UserRepository {
  saveToDatabase(user: User) {
    console.log(`Saving ${user} to database...`);
  }
}
Enter fullscreen mode Exit fullscreen mode

βœ… Now, User is only responsible for storing user data, and UserRepository handles database interactions. Each class has a clear purpose, making it easier to modify and extend.


🟑 O - Open/Closed Principle (OCP)

Definition

"Software entities should be open for extension, but closed for modification."

The Open/Closed Principle (OCP) states that a class should allow new functionality to be added without modifying its existing code. This is crucial for scalable applications, where requirements frequently evolve.

Why is OCP important?

  • Reduces risk when adding new features by avoiding modifications to existing code.
  • Makes code easier to extend and maintain.
  • Encourages the use of polymorphism and interfaces.

❌ Bad Example:

class Discount {
  calculate(price: number, type: string): number {
    if (type === 'student') {
      return price * 0.9;
    } else if (type === 'senior') {
      return price * 0.8;
    }
    return price;
  }
}
Enter fullscreen mode Exit fullscreen mode

πŸ”΄ Every time a new discount type is introduced, we must modify the calculate() method, making it harder to scale and maintain.

βœ… Good Example:

interface DiscountStrategy {
  apply(price: number): number;
}

class StudentDiscount implements DiscountStrategy {
  apply(price: number): number {
    return price * 0.9;
  }
}

class SeniorDiscount implements DiscountStrategy {
  apply(price: number): number {
    return price * 0.8;
  }
}

class PriceCalculator {
  constructor(private discount: DiscountStrategy) {}

  getPrice(price: number): number {
    return this.discount.apply(price);
  }
}
Enter fullscreen mode Exit fullscreen mode

βœ… By using polymorphism, we can introduce new discount types without modifying existing code, making the system flexible and scalable.


🟒 L - Liskov Substitution Principle (LSP)

Definition

"Subtypes must be substitutable for their base types."

The Liskov Substitution Principle (LSP) ensures that a subclass should be able to replace its parent class without breaking the program. This principle helps in maintaining consistency in object-oriented designs.

Why is LSP important?

  • Prevents unexpected behavior when using inheritance.
  • Improves reusability and reliability of code.
  • Helps maintain the correctness of a system.

❌ Bad Example:

class Bird {
  fly() {
    console.log("Flying...");
  }
}

class Penguin extends Bird {
  fly() {
    throw new Error("Penguins can't fly!");
  }
}
Enter fullscreen mode Exit fullscreen mode

πŸ”΄ Here, Penguin is a Bird, but it violates LSP because calling fly() will throw an error. This breaks expected behavior.

βœ… Good Example:

class Bird {
  move() {
    console.log("Moving...");
  }
}

class FlyingBird extends Bird {
  fly() {
    console.log("Flying...");
  }
}

class Penguin extends Bird {}
Enter fullscreen mode Exit fullscreen mode

βœ… Now, Penguin doesn’t have a fly() method, preventing unexpected behavior. The system remains consistent and extendable.


πŸ”΅ I - Interface Segregation Principle (ISP)

Definition

"Clients should not be forced to depend on interfaces they do not use."

The Interface Segregation Principle (ISP) ensures that classes only implement the methods they actually need, rather than being forced to implement unnecessary ones.

Why is ISP important?

  • Prevents classes from having unused methods.
  • Makes code more modular and maintainable.
  • Encourages smaller, more focused interfaces.

❌ Bad Example:

interface Worker {
  work(): void;
  eat(): void;
}

class Robot implements Worker {
  work() {
    console.log("Working...");
  }

  eat() {
    throw new Error("Robots don't eat!");
  }
}
Enter fullscreen mode Exit fullscreen mode

πŸ”΄ Robot has an unnecessary eat() method, violating ISP.

βœ… Good Example:

interface Workable {
  work(): void;
}

interface Eatable {
  eat(): void;
}

class Human implements Workable, Eatable {
  work() {
    console.log("Working...");
  }
  eat() {
    console.log("Eating...");
  }
}

class Robot implements Workable {
  work() {
    console.log("Working...");
  }
}
Enter fullscreen mode Exit fullscreen mode

βœ… Now, each interface only contains relevant methods.


🟣 D - Dependency Inversion Principle (DIP)

Definition

"High-level modules should not depend on low-level modules. Both should depend on abstractions."

DIP encourages using interfaces or abstract classes to reduce dependencies on concrete implementations.

Why is DIP important?

  • Increases flexibility and maintainability.
  • Reduces coupling between classes.
  • Makes testing easier by allowing dependency injection.

βœ… Implementing DIP ensures that we can easily swap implementations without modifying the core logic.


πŸš€ Conclusion

By following SOLID principles, you can write code that is scalable, maintainable, and easier to extend. Let’s build better software! πŸ’ͺπŸ”₯

Heroku

Amplify your impact where it matters most β€” building exceptional apps.

Leave the infrastructure headaches to us, while you focus on pushing boundaries, realizing your vision, and making a lasting impression on your users.

Get Started

Top comments (0)

πŸ‘‹ Kindness is contagious

If you found this article helpful, please give a ❀️ or share a friendly comment!

Got it