Building Robust Codebase: A Deep Dive into SOLID Principles
Tags: cleancode, designpatterns, javascript, solidprinciples
Published: 2024-03-17 11:43:39

We all have heard about the fuss of SOLID principles in one or another. Maybe you’ve picked up a rule or two along the way, or perhaps you’re brand new to the concept altogether. Either way, if you’ve ever felt like you’re not quite getting the full picture, you’re in the right place. First some context.

## History and why it’s useful:-

These rules were first coined by Robert C. Martin to write better code. These rules help us with the following

* Avoid duplicate code
* Easy to maintain codebase
* Scaleable codebase
* Flexible software
* Reduce complexity

Now, you understand why they are useful so, without further ado, let’s dive in.

## S: Single Responsibility Principle(SRP)

As the name suggests, it means that a class or function should have only 1 single responsibility and should not have multiple responsibilities in the first place.

Let’s better understand it with the code


![Neglecting the Single Responsibility Principle](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/p275fb6jsmqyjwoqgp5s.png)

Imagine we’re creating a `User` class, and within it, we’re not only saving the user but also sending a welcome email. According to the Single Responsibility principle, this approach is off track. Why? Well, because our `User` class is currently juggling multiple tasks. It’s responsible for saving the user and also for sending a welcome email. Now, you might not spot an issue with this code at first glance, especially if it’s just a small-scale project. But picture this: what if you’re working with a massive codebase, spanning millions of lines? Suddenly, the problem becomes clearer. Your code ends up tightly coupled, making it a nightmare to make changes. Plus, testing becomes much trickier when one class is trying to do it all.

So, what’s the solution? Well, to address this issue, we can break it down by creating separate classes for each responsibility.


![Upholding Single Responsibility Principle](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ee14prdt3gwk9j0sg1ix.png)

In the example above, we’ve divided the main `User` class into three separate classes, each with its own set of responsibilities. By doing so, we’ve made our code more testable, maintainable, and readable. Now, for instance, our `NotificationService` class is solely responsible for sending notifications to the user, without any additional tasks. This approach leads to more robust code overall.

## O: Open for extension but closed for modification Principle(OCP)

Mouthful I know, let’s understand it first. Open/Closed principle simply says that you should be able to extend the behavior of a class or function without changing its source code(base code).

* **Open for extension:** New functionality can be added without modifying existing code. This allows you to adapt your code to changing requirements without introducing regressions.

* **Closed for modification:** Existing code should not be changed to accommodate new features. This promotes stability and reduces the risk of unintended consequences.

To put it into simple words, the Open/Closed Principle (OCP) suggests that once you’ve written and tested your code, you shouldn’t alter it. Instead, if you need to modify or add functionality, you should extend it without changing the existing codebase.


![Neglecting the open/closed principle](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2gh2mwz13vk14k5quqhk.png)

Consider the scenario with the PaymentProcessor class, tasked with handling payment processing. Currently, it includes a `processPayment` method supporting two payment methods. Now, suppose tomorrow we need to integrate another payment method. We’d find ourselves altering the existing `processPayment` method, which has already been tested and deployed. As we scale, adding multiple payment providers exacerbates(worsen) this issue. Even adding just one more provider could risk unexpected breakages. This is where the Open/Closed Principle comes in, advising against such direct modifications to existing, tested code.

Let’s see how can we change this code to obey the open/closed principle.


![Upholding Open/Closed Principle](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/58vdf32kormm916pk7fz.png)

With the correct implementation, you can add new payment gateways without modifying existing code. For instance, to add support for Google payment provider, you would simply create a new class implementing the `PaymentGateway` interface. No need to change the existing code. No, need to worry that your existing code might break.

## L: Liskov Substitution Principle(LSP)
The Liskov Substitution Principle is a fundamental principle of object-oriented design proposed by Barbara Liskov. It states that objects of a superclass should be replaceable with objects of its subclasses without affecting the correctness of the program. Liskov Substitution Principle is about making sure that when you create a new class that extends another class, it behaves in a way that’s expected by code that uses the original class.

In simple words, subclasses should extend the capability of a parent class and not narrow it down and should not break the behavior of the program.

Let’s see an example


![Neglecting Liskov Substitution Principle](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/abw3li6lt6br61ofg5fs.png)

In this example, both the `Motorcycle` and `Bicycle` classes implement the method `turnOnEngine()`. However, in the case of the `Bicycle` class, it narrows the functionality by throwing an assertion because bicycles do not have engines.

According to the Liskov Substitution Principle, if we substitute an object of the Bicycle class for an object of the Bike class, it would break the functionality. This violation occurs because the Bike class expects the turnOnEngine() method to behave in a certain way, but the Bicycle class does not fulfill this expectation due to its assertion, resulting in a potential runtime error or unexpected behavior.

## I: Interface Segregation Principle(ISP)

This principle focuses on creating smaller, more specific interfaces that cater to distinct client needs. This approach promotes loose coupling and improves code maintainability.

In simple words, Instead of having one large, monolithic interface that encompasses every possible functionality, ISP advocate for breaking it down into smaller, focused interfaces.

Let’s see an example


![Neglecting Interface Segregation Principle](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/dilkjh8457octvksbrpb.png)

In this example, the `Database` interface is a “God Interface” with all possible database operations. This forces both UserManagement and DataAnalytics to implement everything, even if they don’t use all functionalities.

Let’s see how can we fix it


![Upholding Interface Segregation Principle](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/a2sr3x7tw0lrip6asmx2.png)

Here, the Database functionality is split into three smaller interfaces: DatabaseConnection, DataReader, and DataWriter. Classes can now implement only the interfaces they need, promoting better code organization and reducing unnecessary dependencies.

Easy peasy right? I hope so :) Let’s move to the last principle

## Dependency Inversion Principle (DIP)

DIP emphasizes relying on abstractions (interfaces or abstract classes) rather than concrete implementations. In simple words, class should be dependent on interfaces rather than concrete classes. This promotes loose coupling, improves testability, and enhances code maintainability.

Let’s consider the example below


![Neglecting Dependency Inversion Principle](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/mq4gbj30qxfxbc0o2c45.png)

In the above example, `NotificationService` directly depends on `EmailService`, violating the Dependency Inversion Principle. If we want to send notifications through a different medium (e.g., SMS), we would need to modify `NotificationService` leading to tightly coupled code.

To fix this. Let’s consider the example below


![Upholding Dependency Inversion Principle](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/tymh6cgm9kk1bm7p0zwp.png)

In this example, `NotificationService` depends on the abstraction `NotificationSender` not on `EmailService` directly. This adheres to the Dependency Inversion Principle. Now, `NotificationService` can send notifications through any medium by providing an implementation of `NotificationSender` such as `EmailService` `SMSService` etc., without modifying `NotificationService` itself.

Epic right?

So in conclusion, the SOLID principles serve as a foundational guide for writing maintainable, scalable, and flexible software. By adhering to these principles, developers can design code that is easier to understand, modify, and extend, leading to more robust and reliable systems.

From the Single Responsibility Principle (SRP), which encourages classes to have a single reason to change, to the Dependency Inversion Principle (DIP), which promotes decoupling and flexibility through abstraction, each SOLID principle offers valuable insights into effective software design.

And remember, these principles aren’t limited to just classes; they’re also applicable in functional programming. So, whether you’re more inclined towards OOP or Functional programming paradigms, you can always rely on these principles to craft robust, clean, flexible, and scalable code that both you and your colleagues will surely appreciate! ;)