DEV Community

Flames ✨
Flames ✨

Posted on

Mastering the 5 SOLID Principles in Software Engineering.

Image descriptionIntroduction:
Software engineering is an ever-evolving field, and creating high-quality software that is maintainable, scalable, and robust is essential for success. The SOLID principles offer valuable guidelines for achieving these goals. In this article, we will explore the 5 SOLID principles and provide clear explanations along with JavaScript code examples to help newcomers understand and apply these principles in their own projects.

1. Single Responsibility Principle (SRP):
The Single Responsibility Principle (SRP) states that a class should have only one reason to change. By adhering to this principle, we ensure that each class has a single responsibility or job, making the code more maintainable and testable.

Example:
Consider a simple JavaScript class called User responsible for user management:

class User {
  constructor(name, email) {
    this.name = name;
    this.email = email;
  }

  getName() {
    return this.name;
  }

  getEmail() {
    return this.email;
  }

  save() {
    // Code to save user to the database
  }

  sendEmail(message) {
    // Code to send an email to the user
  }
}
Enter fullscreen mode Exit fullscreen mode

In this example, the User class handles both user data management and email sending. To adhere to the SRP, we can extract the email sending functionality into a separate class:

class User {
  constructor(name, email) {
    this.name = name;
    this.email = email;
  }

  getName() {
    return this.name;
  }

  getEmail() {
    return this.email;
  }

  save() {
    // Code to save user to the database
  }
}

class EmailService {
  sendEmail(user, message) {
    // Code to send an email to the user
  }
}
Enter fullscreen mode Exit fullscreen mode

By separating concerns, we achieve better code organization, easier testing, and improved maintainability.

2. Open-Closed Principle (OCP):
The Open-Closed Principle (OCP) states that software entities should be open for extension but closed for modification. This principle encourages us to design our code in a way that allows new functionality to be added without modifying existing code.

Example:
Suppose we have a Shape base class and multiple derived classes representing different shapes:

class Shape {
  calculateArea() {
    throw new Error('Method not implemented.');
  }
}

class Circle extends Shape {
  constructor(radius) {
    super();
    this.radius = radius;
  }

  calculateArea() {
    return Math.PI * this.radius * this.radius;
  }
}

class Rectangle extends Shape {
  constructor(width, height) {
    super();
    this.width = width;
    this.height = height;
  }

  calculateArea() {
    return this.width * this.height;
  }
}
Enter fullscreen mode Exit fullscreen mode

The code follows the OCP because if we want to add a new shape, we can create a new derived class without modifying the existing Shape or other shape classes.

3. Liskov Substitution Principle (LSP):
The Liskov Substitution Principle (LSP) states that objects of a superclass should be replaceable with objects of its subclasses without affecting the correctness of the program. In simpler terms, any derived class should be able to substitute its base class without breaking the program's behavior.

Example:
Consider a scenario where we have a Bird base class and derived classes Duck and Ostrich:

class Bird {
  fly() {
    throw new Error('Method not implemented.');
  }
}

class Duck extends Bird {
  fly() {
    console.log('Flying like a duck!');
  }
}

class Ostrich

extends Bird {
  run() {
    console.log('Running like an ostrich!');
  }
}
Enter fullscreen mode Exit fullscreen mode

In this example, Duck and Ostrich inherit from the Bird class, and they each have a unique implementation of the fly method. The Duck class can substitute the Bird class without affecting the program's behavior, while the Ostrich class cannot. Therefore, we have violated the LSP, and to fix this, we can remove the fly method from the Bird class and move it to a new interface or base class like FlyingBird.

4. Interface Segregation Principle (ISP):
The Interface Segregation Principle (ISP) states that clients should not be forced to depend on interfaces they do not use. This principle encourages us to create small and cohesive interfaces that are specific to each client's needs, instead of creating large and bloated interfaces that cater to every possible client.

Example:
Consider a scenario where we have a Printer interface that defines the methods print, scan, and fax. However, not every client needs all these methods.

interface Printer {
  print(content: string): void;
  scan(): void;
  fax(number: string, content: string): void;
}

class DesktopPrinter implements Printer {
  print(content: string) {
    console.log('Printing from desktop printer');
  }

  scan() {
    console.log('Scanning from desktop printer');
  }

  fax(number: string, content: string) {
    console.log('Faxing from desktop printer');
  }
}

class MobilePrinter implements Printer {
  print(content: string) {
    console.log('Printing from mobile printer');
  }

  scan() {
    console.log('Scanning from mobile printer');
  }

  fax(number: string, content: string) {
    throw new Error('Method not implemented.');
  }
}
Enter fullscreen mode Exit fullscreen mode

In this example, the DesktopPrinter class implements all the methods of the Printer interface, while the MobilePrinter class only implements the print and scan methods. By using ISP, we have created two smaller and more focused interfaces: PrintOnly and PrintScan, which clients can use based on their specific needs.

5. Dependency Inversion Principle (DIP):
The Dependency Inversion Principle (DIP) states that high-level modules should not depend on low-level modules but instead depend on abstractions. This principle encourages us to use interfaces or abstract classes to create a separation of concerns and decouple high-level and low-level modules.

Example:
Consider a scenario where we have a PaymentProcessor class that handles payment processing. The PaymentProcessor class has a dependency on the PaymentGateway class for communication with the payment gateway API.

class PaymentGateway {
  constructor(apiKey) {
    this.apiKey = apiKey;
  }

  processPayment(amount) {
    // Code to process payment
  }
}

class PaymentProcessor {
  constructor(paymentGateway) {
    this.paymentGateway = paymentGateway;
  }

  processPayment(amount)
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/dtizjgquh7i1ftn9re1c.jpg) {
    this.paymentGateway.processPayment(amount);
  }
}

const paymentGateway = new PaymentGateway('API_KEY');
const paymentProcessor = new PaymentProcessor(paymentGateway);

paymentProcessor.processPayment(100);
Enter fullscreen mode Exit fullscreen mode

In this example, the PaymentProcessor class depends directly on the PaymentGateway class, violating the DIP. We can fix this by creating an interface or an abstract class that PaymentGateway implements, and PaymentProcessor depends on that abstraction.

Conclusion:
The SOLID principles provide us with guidelines for creating maintainable, scalable, and robust software. By following these principles, we can create code that is easier to understand, test, and extend. Understanding and applying the SOLID principles is crucial for developers at any level, as it promotes good software design practices and facilitates collaboration in larger codebases. By using JavaScript code examples, we have illustrated how each SOLID principle can be implemented, enabling newcomers to grasp these principles and apply them effectively in their own projects.

Top comments (2)

Collapse
 
faizbyp profile image
Faiz Byputra

Thank you very much for the explanation!

Collapse
 
iflames_1 profile image
Flames ✨

You're welcome. 🥂