Solid Principle
Introduction
The SOLID principles were developed by Robert C. Martin in a 2000 essay, “Design Principles and Design Patterns,” although the acronym was coined later by Michael Feathers. In his essay, Martin acknowledged that successful software will change and develop. As it changes, it becomes increasingly complex. Without good design principles, Martin warns that software becomes rigid, fragile, immobile, and vicious. The SOLID principles were developed to combat these problematic design patterns.
Goal
The broad goal of the SOLID principles is to reduce dependencies so that engineers change one area of software code without impacting others. Additionally, they’re intended to make designs easier to understand, maintain, and extend. Ultimately, these design principles make it easier for software engineers to avoid issues and build adaptive, effective, and agile software.
SOLID Acronym – it is an acronym that stands for five key design principles:
- Single Responsibility Principle (SRP)
- Open Closed Principle (OCP)
- Liskov Substitution Principle (LSP)
- Interface Segregation Principle (ISP)
- Dependency Inversion Principle (DIP)
Software engineers commonly use all five and provide some important benefits for developers. Advantages of SOLID Principles:-
- Achieve the reduction in the complexity of the code.
- Increase readability, extensibility, and maintenance.
- Increase scalability, code flexibility, and readability.
- Achieve better testability.
- Reduce tight coupling.
- Reduce error and implement Reusability.
- Increase Parallel development.
Let’s discuss all five principles with some examples. Here, I will be using Java programming language for the examples.
Single Responsibility Principle (SRP)
The Single Responsibility Principle states, “Each software module or class should have only one reason to change. “ In other words, we can say that each module or class should have only one responsibility to do. We must design the software so that everything in a class or module is related to a single responsibility. That is not to say that your class should only have one method or property; you can have multiple members (methods or properties) as long as they are all related to a single responsibility or functionality. As a result of the Single Responsibility Principle, the classes become smaller and cleaner, making them easier to maintain.
In this example, the BankService class has a single responsibility, which is to deposit and withdraw of Money. The PrinterService class has a single responsibility: printing the Passbook. Both classes focus on a single, well-defined task and have only one reason to change. This makes the code easier to understand, maintain, and test.
Open-Closed Principle (OCP)
The Open/Closed Principle is one of the SOLID principles of software design, which suggests that software entities such as classes, modules, and functions should be open for extension but closed for modification. This principle states that you should be able to add new functionality to a system without having to modify existing code. In other words, you should be able to extend the behavior of a system without changing its existing codebase. This is important because modifying existing code can lead to unforeseen consequences and introduce bugs, while extending the behavior of a system can improve its functionality without negatively affecting its existing code. Let’s consider a Java example to illustrate the Open/Closed Principle. Suppose we have a basic notification system to send the user the OTP and transaction report.
Now, We have the ability to send the OTP and transaction report via different mediums to the user, without modifying the existing code. To do this, we can create a new class that extends the NotificationService class and implement the following methods:
This new EmailNotificationService and SMSNotificationService class adds the ability to share the OTP. It reports to the user via a different medium, but it does so by extending the NotificationService class rather than modifying it. This means that the NotificationService class remains unchanged, and we can continue to use it in the same way as before.
As you can see, we can use the Email and SMSNotificationService classes interchangeably because they both have methods with the same signature.
In summary, the Open/Closed Principle is an important software design principle that suggests that software entities should be open for extension but closed for modification. In the example above, we demonstrated how we can extend the behavior of a notification system by adding a new EmailNotificationService class that extends the existing NotificationService class, without modifying its existing code base.
Liskov Substitution Principle
The Liskov Substitution Principle (LSP) is a principle in object-oriented programming that states that objects of a superclass should be replaceable with objects of a subclass without affecting the correctness of the program. This means that a subclass should be a subtype of its superclass and that objects of the subclass should be able to be used wherever objects of the superclass are expected without introducing any new bugs or causing any existing functionality to break. The LSP is important for creating maintainable and flexible code. If a subclass is not a proper subtype of its superclass, then using an object of the subclass in place of an object of the superclass can cause unexpected behavior, leading to bugs that are difficult to track down and fix. To ensure that a subclass is a proper subtype of its superclass, it should fulfill the following requirements:
- The subclass should have all the properties and methods of the superclass.
- The subclass should not have any new properties or methods that are not in the superclass.
- The subclass should not reduce the visibility of any properties or methods in the superclass.
- The subclass should not change the behavior of any properties or methods in the superclass in a way that would break the existing code that uses the superclass.
To illustrate the LSP, let’s consider the Java example. Suppose we have a class SocialMedia Containing different features like chatWithFriend, publishPost, sendPhotosAndVideos, etc. When classes like Instagram, Facebook & WhatsApp are going to extend the SocialMedia, then we have to extend those features as well, which are not part of that platform.
To fix this problem and adhere to the Liskov Substitution Principle, we need to change the design of the SocialMedia class and break it into more abstract classes like PostMediaManager, SocialVideoCallManager etc. Now, we will create different-2 social media platform classes like Facebook, Instagram, etc. Facebook will extend those abstract classes that are applicable to it only.
By using a separate SocialMedia class, we can ensure that a SocialMedia object can be substituted for a Facebook, Instagram, or Whatsapp object without violating the correctness of the program. Following the LSP, we can create a flexible and maintainable code base, where objects of different classes can be used interchangeably without causing any unexpected behavior. This makes it easier to extend and modify our code as needed without introducing any new bugs.
Interface Segregation Principle (ISP)
It states that a client should not be forced to depend on interfaces it does not use. In other words, it is better to have multiple smaller interfaces tailored to specific needs rather than a single large interface that tries to cover everything. To illustrate the Interface Segregation Principle, let’s consider a Java example. Suppose we have a UPIPayments interface, with methods to pay, get a scratch cards and get cashback:
To adhere to the Interface Segregation Principle, we can split the UPIPayments interface into two smaller interfaces, one for methods that apply to every platform and another for a specific one, namely CashBackManager, and implement accordingly.
In this new implementation, the PhonePe class only needs to implement the UPIPayments interface, and the GooglePay class can implement both UPIPayments and CashBackManager interfaces. This adheres to the Interface Segregation Principle, as each class is only dependent on the interfaces that it uses. The Interface Segregation Principle is an important principle of software design that encourages the use of smaller, more focused interfaces instead of large, monolithic ones. In the Java example above, we demonstrated how to refactor the UPIPayments interface to adhere to the Interface Segregation Principle and avoid unnecessary coupling and unexpected behavior.
Dependency Inversion Principle (DIP)
It states that high-level modules should not depend on low-level modules. Instead, both should depend on abstractions. This promotes loose coupling and flexibility, making it easier to make changes without affecting other parts of the system. To illustrate the Dependency Inversion Principle, let’s consider a Java example. Suppose we have a BankCard class that needs to use a DebitCard/CreditCard class to do the transaction in the shopping mall. In this example, the BankCard class represents a high-level module, and the DebitCard/CreditCard class represents a low-level module.
In this implementation, the BankCard class directly instantiates a DebitCard/CreditCard object in its constructor. This violates the Dependency Inversion Principle because the BankCard class is dependent on a low-level module, making it inflexible and tightly coupled.
To adhere to the Dependency Inversion Principle, we can use an abstraction, such as an interface or an abstract class, to decouple the BankCard class from the DebitCard/CreditCard class. We can define a DebitCard/CreditCard interface to do the transaction and then inject an instance of this interface into the BankCard class:
In this new implementation, the BankCard class is no longer dependent on the DebitCard/CreditCard class but rather on the DebitCard/CreditCard interface. We can now create multiple implementations of the DebitCard/CreditCard interface, which can be used to do the transaction differently. This makes the BankCard class more flexible and easier to change, as it is no longer tightly coupled to the DebitCard/CreditCard.
Conclusion
In the Java example above, we demonstrated how to use an abstraction, such as an interface, to decouple a high-level module from a low-level module, making the code more flexible and easier to change.
To stay up-to-date with the ever-changing world of APIs, Microservices, and Digital Transformation. Follow us on social media for more updates.