Mastering the 5 SOLID Principles in Software Engineering.
Tags: webdev, programming, javascript, productivity
Published: 2023-05-16 23:27:16


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/roytp60a3ealj1rty2cn.jpg)**Introduction:**
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:

```javascript
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
  }
}
```

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:

```javascript
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
  }
}
```

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:

```javascript
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;
  }
}
```

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`:

```javascript
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!');
  }
}
```

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.

```javascript
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.');
  }
}
```

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.

```javascript
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);
```

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.