blog-cover

🧐 Why SOLID Principles?

As software developers, we strive to create systems that are robust, maintainable, and easy to scale. The SOLID principles offer a foundation for writing clean, well-structured code by encouraging single-purpose classes, extensible designs, and minimal dependencies.

Here's a breakdown of each principle with examples and insights into their application:

Each principle includes a difficulty rating (Easy, Medium, or Hard) to reflect its understanding complexity.


πŸ“œ 1. Single Responsibility Principle (SRP) Medium

Definition: A class should have only one reason to change, meaning it should have a single responsibility or purpose.

/** Violates SRP **/
class User {
  saveToDatabase() {}
  logUserActivity() {}
}
/** Follows SRP **/
class UserRepository {
  saveToDatabase(user: User) {}
}
class Logger {
  logUserActivity(user: User) {}
}

🚫 Violates SRP: Handles multiple responsabilities.
βœ… Follows SRP: Separate concerns into different classes like UserRepository and Logger.


πŸ“¦ 2. Open/Closed Principle (OCP) Hard

Definition: Software entities (classes, modules, functions) should be open for extension but closed for modification.

/** Violates OCP **/
class Shape {
  draw(shapeType: string) {
    if (shapeType === 'circle') {
      drawCircle();
    } else if (shapeType === 'square') {
      drawSquare();
    }
  }
}
/** Follows OCP **/
abstract class Shape {
  abstract draw(): void;
}
class Circle extends Shape {
  draw() { /* draw circle */ }
}
class Square extends Shape {
  draw() { /* draw square */ }
}

🚫 Violates OCP: Modifying existing code for new behaviours.
βœ… Follows OCP: Add new behaviours without modifying existing code.


πŸ”„ 3. Liskov Substitution Principle (LSP) Hard

Definition: Subtypes must be substitutable for their base types without altering the correctness of the program. In other words, a derived class must be able to replace its parent class without causing unexpected behavior.

How to identify a violation: If overriding a method in the subclass breaks assumptions about the parent class, this violates LSP. Subtypes must maintain the behavior expected by the base type.

/** Violates LSP **/
class Rectangle {
  setWidth(width: number) { this.width = width; }
  setHeight(height: number) { this.height = height; }
  getArea() { return this.width * this.height; }
}
class Square extends Rectangle {
  setWidth(width: number) {
    this.width = width;
    this.height = width; // Breaks LSP: unexpected behavior
  }
}
/** Follows LSP **/
abstract class Shape {
  abstract getArea(): number;
}
class Rectangle extends Shape {
  constructor(private width: number, private height: number) {
    super();
  }
  getArea() { return this.width * this.height; }
}
class Square extends Shape {
  constructor(private side: number) {
    super();
  }
  getArea() { return this.side * this.side; }
}

Regarding this example: A Square class inheriting from Rectangle but behaving differently when setting its width or height. The base class assumes independent width and height, but the Square class enforces equal sides, leading to unexpected behavior.

🚫 Violates LSP: When a derived class alters assumptions or expected behaviors defined in the base class, it breaks substitutability.
βœ… Follows LSP: Ensure that subtypes maintain consistent behavior and respect the expectations set by their base type.


πŸŽ›οΈ 4. Interface Segregation Principle (ISP) Easy

Definition: A class should not be forced to implement interfaces it doesn’t use. Instead, create specific, smaller interfaces.

/** Violates ISP **/
interface MultifunctionDevice {
  print(): void;
  scan(): void;
  fax(): void;
}
/** Follows ISP **/
interface Printer {
  print(): void;
}
interface Scanner {
  scan(): void;
}

🚫 Violates ISP: A single interface forcing implementations to include unused or irrelevant methods.
βœ… Follows ISP: Divide interfaces into smaller, specific ones to ensure implementations only include relevant methods.


πŸ”— 5. Dependency Inversion Principle (DIP) Medium

Definition: High-level modules should not depend on low-level modules. Both should depend on abstractions (e.g., interfaces).

/** Violates DIP **/
class EmailService {
  sendEmail() {
    /* email logic */
  }
}
class Notification {
  private emailService = new EmailService();
  notify() {
    this.emailService.sendEmail();
  }
}
/** Follows DIP **/
interface MessageService {
  sendMessage(): void;
}
class EmailService implements MessageService {
  sendMessage() {
    /* email logic */
  }
}
class Notification {
  private messageService: MessageService;
  constructor(private messageService: MessageService) {
    this.messageService = messageService;
  }
  notify() {
    this.messageService.sendMessage();
  }
}

🚫 Violates DIP: High-level classes directly depend on low-level implementations.
βœ… Follows DIP: Use abstractions to decouple dependencies.


garnalds

Resources

  • "Nothing is Something": A highly recommended video that introduces the Null Object Pattern in a clear and practical way. It also features an excellent example of how inheritance can go wrong and the transitioning from inheritance to composition, making it a must-watch for anyone looking to deepen their understanding of clean design principles. Watch it here
  • "Uncle Bob on SOLID Principles": A video that covers object-oriented design concepts and touches on the SOLID principles near the end. A good watch for understanding the foundations of clean code. Watch it here

Conclusion

πŸ₯° By following these principles, you can create software that is easier to maintain, scale, and extend. Whether you're building a small app or a complex system, SOLID principles serve as a guide to avoid technical debt and enhance code quality.

Start applying them today and watch your codebase transform! πŸŽ‰