Hanabi Logo

Discover Hana AI

Hana, Google Chat integrated versatile AI assistant developed by Hanabi Technologies
Hanabi Logo

Kasavanahalli, Bengaluru, India, 560035

sales@hanabitech.com

HELLO

BehanceGmailInstagramLinkedInMedium

Mastering SOLID Principles: The Key to Crafting RobustSoftware

Hanabi Coding Standards

Writing clean, maintainable, and extensible code is a constant pursuit during software development. We strive to create software that is easy to understand, change, and scale, regardless of its complexity. The SOLID principles are a set of guiding principles that act as a compass, pointing developers toward the path of robust and well-structured code.

SOLID principles were first introduced by the famous computer scientist Robert C. Martin (Uncle Bob). They represent five fundamental principles:

  • The Single Responsibility Principle
  • The Open-Closed Principle
  • The Liskov Substitution Principle
  • The Interface Segregation Principle
  • The Dependency Inversion Principle

These principles serve as a blueprint for designing software systems that are flexible, modular, and resistant to the ever-changing demands of the development landscape.

In this blog, We'll delve into each principle, dissecting its core concepts and presenting real-life examples that resonate with both seasoned developers and those new to the world of software development.

By the end of this series, you'll have a solid grasp of the SOLID principles and how they can shape your codebase, enabling you to build software that is easier to understand, maintain, and enhance over time.

So, fasten your seat belts and get ready to embark on a transformative journey as we unravel the secrets behind the SOLID principles and unlock the door to clean, elegant, and maintainable code.

The Single Responsibility Principle (SRP)

SRP states that a class or module should have only one reason to change. In simple words, it means that a class should have a single responsibility or purpose.

To understand SRP, let's consider a JavaScript example:

In the above example, we have a UserService class that performs three different tasks: retrieving users from the database, saving users to the database, and sending emails. This violates the SRP because the class has multiple responsibilities.

To adhere to SRP, we should refactor the code and split the responsibilities into separate classes:

In the refactored code, we have separate classes for each responsibility. The UserRetriever class handles retrieving users, the UserSaver class handles saving users, and the EmailSender class handles sending emails. Each class has a single responsibility, making the code easier to understand, maintain, and extend.

By adhering to SRP, we ensure that our code is more focused, modular, and maintainable. If we need to make changes related to user retrieval, we only need to change the UserRetriever class, without affecting the other functionalities. This principle helps in reducing code complexity and promoting better organization and separation of concerns in our applications.

Advantages

  • Improved code organization: Each class or module has a clear and focused responsibility, making the codebase easier to navigate and understand.
  • Enhanced code maintainability: Changes or updates to a specific responsibility can be isolated to a single class or module, minimizing the impact on other parts of the system.
  • Code reuse: Well-defined responsibilities encourage reusable code components, as they are designed to handle specific tasks efficiently.

The Open-Closed Principle (OCP)

OCP states that software entities (classes, modules, functions) should be open for extension but closed for modification. In simple terms, it means that we should design our code in a way that allows us to add new functionality without having to modify the existing code.

Let's understand the OCP with a JavaScript example:

In the above example, we have a base class called Shape that defines a common interface for calculating the area. It has a method area() that is meant to be overridden by subclasses. This class follows the OCP because it is open for extension. We can add new shapes by creating new classes that extend Shape without modifying the existing code.

For instance, let's add a new shape called Triangle:

With the addition of the Triangle class, we can calculate the area of a triangle without modifying the Shape class or any of the existing shape classes. The code remains closed for modification. We are extending the behavior without changing the existing codebase.

By adhering to the Open-Closed Principle, we promote code that is easier to maintain, as modifications are localized to new classes rather than altering existing ones. Additionally, it enables us to introduce new features or accommodate changes in requirements without the risk of introducing bugs in previously working code.

Advantages

  • Code extensibility: New functionality can be added to the system without modifying existing code, reducing the risk of introducing bugs or breaking existing functionality.
  • Easy to maintain: By keeping existing code closed for modification, it becomes easier to maintain and test as there is less chance of unintended side effects.
  • Improved code stability: Modifications to existing code can introduce errors, but following OCP mitigates this risk by allowing extensions without direct modification.

The Liskov Substitution Principle (LSP)

LSP states that objects of a superclass should be replaceable with objects of its subclasses without affecting the correctness of the program. In other words, if a program works correctly with a base class object, it should also work correctly with any of its derived class objects.

To understand this principle, let's consider a simple JavaScript example:

In this example, we have a Rectangle class with a setWidth() method to set the width, setHeight() method to set the height, and getArea() method to calculate the area. The Square class extends Rectangle, and its setWidth() and setHeight() methods override the base class methods to ensure the width and height are always equal, effectively making it a square.

Now, let's see how the LSP comes into play. According to LSP, we should be able to substitute a Rectangle object with a Square object without breaking the behavior of the program.

In the above code, we have a printArea() function that takes a Rectangle object as an argument. We first create an instance of Rectangle and pass it to the function, which correctly calculates the area as 20 (width: 5, height: 4). Then, we create an instance of Square and pass it to the same function, which calculates the area as 16 (width and height: 4).

This example demonstrates the violation of the Liskov Substitution Principle. The Square class, although a subclass of Rectangle, does not behave like a typical rectangle. It violates the principle because it overrides the base class methods in a way that alters the expected behavior. This substitution results in different outputs, breaking the correctness of the program.

To adhere to LSP, we should reconsider the class hierarchy or behavior of our objects. In this case, it would be more appropriate to have separate Rectangle and Square classes, without one inheriting from the other, to ensure correct behavior and adherence to LSP.

LSP reminds us to design our classes and inheritance hierarchies carefully, ensuring that derived classes can substitute base classes without altering the expected behavior of the program.

Advantages

  • Increased code flexibility: Subclasses can be used interchangeably with their base class, allowing for polymorphic behavior and facilitating code reuse.
  • Improved modularity: By adhering to LSP, code modules become more interchangeable and pluggable, enabling easier maintenance and extensibility.
  • Enables design by contract: LSP encourages the definition of clear contracts (interfaces or base classes) that govern the behavior of derived classes, leading to more robust and predictable systems.

The Interface Segregation Principle (ISP)

ISP states that clients should not be forced to depend on interfaces they do not use. In simpler terms, it suggests that we should create smaller and more focused interfaces rather than having large interfaces that cover a wide range of functionalities.

To understand ISP, let's consider a JavaScript example:

In the above example, the Animal class has three methods: fly(), swim(), and run(). Both the Bird and Fish classes extend Animal and inherit these methods. However, the implementation of these methods in the derived classes may not be appropriate for all types of animals.

The problem with this design is that all clients that use the Animal interface, including Bird and Fish, are forced to depend on all three methods (fly(), swim(), and run()) regardless of whether they need them or not.

To adhere to the ISP, we can refactor the code by creating separate interfaces for different functionalities:

In this improved design, we have created separate interfaces (Flyable, Swimmable, and Runnable) that represent specific functionalities. The Bird class implements the Flyable and Runnable interfaces, while the Fish class implements the Swimmable interface.

By segregating the interfaces, we allow clients to depend only on the interfaces they need. For example, a client that only requires flying functionality can now depend on the Flyable interface without being forced to depend on swimming or running methods.

Applying the Interface Segregation Principle helps in creating more modular, maintainable, and flexible code. It allows for better code reuse and reduces the likelihood of clients being burdened with unnecessary dependencies.

Advantages

  • Reduced dependencies: Clients depend only on the interfaces they actually use, minimizing unnecessary dependencies and potential code coupling.
  • Enhanced readability: Smaller, focused interfaces make it easier to understand the expected behavior of a component, making code more readable and self-explanatory.
  • Improved testability: With interfaces tailored to specific client needs, it becomes easier to create focused and isolated unit tests, increasing the overall test coverage and maintainability.

The Dependency Inversion Principle (DIP)

DIP is a software design principle that suggests that high-level modules should not depend on low-level modules. Instead, both should depend on abstractions or interfaces. This principle promotes loose coupling, flexibility, and easier maintenance of the codebase.

In simpler terms, DIP encourages us to depend on abstractions rather than concrete implementations. It allows us to write code that is more modular, reusable, and easier to test and maintain.

Let's understand DIP with a JavaScript example:

In the above example, the NotificationService class represents a high-level module that sends notifications. However, it directly creates an instance of the EmailService class, which represents a low-level module responsible for sending emails.

This design violates the DIP because the NotificationService depends on a concrete implementation (EmailService) rather than an abstraction or interface. It tightly couples the high-level module with the low-level module, making it difficult to switch or replace the implementation of the email service.

To adhere to the DIP, we can refactor the code by introducing an abstraction or interface:

In the refactored code, we define an abstraction INotificationService that declares the sendNotification method. The EmailService class now extends this abstraction and provides its own implementation.

The NotificationService class, representing the high-level module, now depends on the abstraction (INotificationService) rather than the concrete implementation (EmailService). This inversion of dependencies allows us to easily substitute different implementations of the INotificationService, such as a SMSNotificationService or PushNotificationService, without modifying the high-level module.

By following the DIP, we achieve a more flexible and maintainable codebase, as we can easily swap dependencies and decouple different modules. making our code easier to maintain, modify, and extend.

Advantages

  • Code modularity: High-level modules and low-level modules become more independent and interchangeable, enabling better organization and separation of concerns.
  • Easy integration of new functionality: DIP allows new implementations of dependencies to be introduced without modifying the existing codebase, making it easier to integrate third-party libraries or switch between different implementations.
  • Testability and mocking: By depending on abstractions, it becomes simpler to mock dependencies during testing, facilitating unit testing and improving test coverage.

Conclusion

This blog post provides simple JavaScript examples to demonstrate the concepts behind each SOLID principle. This helps our team @Hanabitechnologies to write more maintainable, extensible, and robust code and contribute to creating high-quality software systems for our clients.