In Object Oriented Programming(OOP) Concept, S.O.L.I.D. is the most favored Design Principle that helps inventors write further readable, justifiable, scalable, and applicable law. It’s named on a mnemonic acronym for five design principles listed below
S) Single Responsibility Principle (S.R.S.)
O) Open/ Closed Principle (O.C.P.)
L) Liskov’s Substitution Principle (L.S.P.)
I) Interface Segregation Principle (I.S.P.)
D) Dependency Inversion Principle (D.I.P.)
This blog will cover all the over-mentioned principles in Object Oriented Design and will make us understand why it’s preferred the most in Software Design and Development. We’re also going to explore how to apply these principles effectively in our operation development cycle with practical exemplifications using C#.
Advantages of using S.O.L.I.D. Design Principles
All inventors have a natural tendency to start developing operations grounded on our conscious experience and knowledge. Over a period of time, when we move forward with the operation’s development sprints, the law starts arising with some or the other issues that need to be addressed, and a lot of rewriting occurs in our law.
The Architectural Design of the System should be as clean and scalable as possible to keep the critical effects that any professional inventor should devote their time to would fluently understand and work on it, and that’s where the S.O.L.I.D. Design Principles come to play. It helps inventors exclude design taste and make stylish designs for a set of features or forthcoming features.
From designing to developing an operation, we can work the advantages of S.O.L.I.D. Principles to write the law in the correct manner. Let’s have a look at the Advantages of following the S.O.L.I.D. Principles by using it from the Base of the Architecture of the Software to the Updation of the point in our software.
Maintainability As businesses start growing and the request requires further changes or updates, the software design should be acclimated to the unborn variations at ease.
Testability: While designing and developing a large- scale software/ operations, it’s necessary to make one system that facilitates testing each functionality beforehand and fluently.
Inflexibility and Scalability These are the foremost pivotal corridors of any enterprise operation. As a result, the system design should be adaptable to any after updates or extensions of new features easily and efficiently.
Resemblant Development It can be challenging for all the platoon members to work on the same module at the same time, so the software needs to be broken down into colorful modules which allow different brigades to work singly and contemporaneously.
Single Responsibility Principle(S.R.P.)
Uncle Bob states that “Every Software Module should have only O.N.E. Reason to Change.”
A class with a lot of functionality won’t be conceptually cohesive, giving it several reasons to change. Reducing the number of times to modify a class module is essential. However, we only need to modify that specific class, If the job’s specifications change over time. This change is less likely to break the whole operation since other classes continue to serve ahead. As a result, classes would come lower, cleaner, and therefore easier to maintain and acclimatize to any change briskly.
This principle can be easily understood with an illustration of BankService, which performs the following operations
- Passbook Printing
- Get Loan Information
- Shoot OTP
Here, in this example, the class has multiple reasons to change.
Let’s say the bank wants to add 2 more services in loans, like gold loans and student loans, in the future. In this case, the bank service class needs to be modified again. Similarly, again in the future, the bank wants to add Phone OTP Verification as another mode of Verification. So, again the BankService class needs to be modified.
In this scenario, we cannot consider that the BankService class is following the Single Responsibility Principle as it consists of too many responsibilities or tasks to perform.
So, to achieve the goal of the Single Responsibility Principle, we should always implement Multiple Classes with Single Responsibility each.
For Example –
We can move the Transaction Operations-related code into a separate class called Transaction.
Similarly, for Passbook Printing, create a separate class called Printer Service.
Similarly, Loan Services related jobs in LoanService class.
Finally, OTP Verification related jobs in NotificationService class.
Now, you will be able to extend all the above classes and use their functionalities and update any particular class as required.
Concluding, all the classes are now properly organized, where any future modifications can impact only one class where the particular responsibility needs to be updated and can adapt quickly. Therefore, each class now has Single Responsibility to perform, which is exactly following the Single Responsibility Principle.
Open/Closed Principle (OCP)
“Software entities (classes, modules, functions, etc.) should be open for extension but closed for modification.”
The Modules that strictly follow the Open/Closed Principle include two key attributes, namely:
1. Open for Extension
The module’s behavior can be extended when the requirements of the application change. We can extend the module with new behaviors that can adapt to those changes.
2. Closed for Modification
Once the module has been implemented and passed all the Test Cases of Unit Testing, it must be closed for modification or code refactoring until any bugs are detected. We must not manipulate the module for any further feature update or extension as it has already passed the Unit Testing of that module until it is very mandatory to be identified.
Let’s understand the Open/Closed Principle with the Previous Example taken, i.e., Notification Service. Let’s say the bank wants to introduce Notification Service via Mobile Number and Whatsapp Messenger. Here, we should not introduce the Mobile Number Notification or Whatsapp Messenger but must prefer our code that can be reusable in such a way that it can be extended on other applications too by using abstraction.
Let’s now design an Interface for the notification service.
Now, we should extend the NotificationService Interface to EmailNotification, MobileNotification, and WhatsappNotification classes.
This abstraction will refrain from the whole code modification and will only affect the feature of the particular update. Also, to add any new NotificationService Feature, we can now create a separate class for it and adapt it.
This approach will never affect the existing code/application and will follow the Open / Closed Principle.
Liskov’s Substitution Principle
“You should be able to use any derived classes instead of the parent class and make it behave in the same manner without any modification.”
Liskov’s Substitution Principle states that when we have a parent class and a child class in our application, the child class in our application can be a substitution of the parent class without changing the correctness of the application.
This principle is based on Inheritance concepts so let’s understand this with an example.
Let’s consider we have an abstract class in our application called SocialMedia, which supports all social media activity for a user to entertain them as follows:
Social Media can have multiple implementations or can have multiple children like Facebook, Whatsapp, Instagram, Snapchat, Twitter, etc.
Now, let’s assume Facebook wants to use the features or functionality of the SocialMedia class.
In the 20th century, everyone used the Facebook Application and all the above-mentioned features available, so here we can consider Facebook as a complete substitute for the SocialMedia class, where both can be replaced without any interruption.
Now, let’s discuss the Whatsapp class.
Due to the publish post() method, the child WhatsappMessenger class is not a substitute for the parent SocialMedia class because WhatsappMessenger doesn’t support uploading photos and videos for friends. It’s just a chatting application, so we can conclude it doesn’t follow Liskov’s Substitution Principle.
Similarly, Snapchat doesn’t support the group video call() feature, so we can say that the child Snapchat class is not a substitute for the parent SocialMedia class.
To overcome this issue, we can create SocialMedia Interface and segregate specific functionality to separate classes to follow the principle.
Here, we have segregated specific functionalities to separate classes to follow the principle.
Now, it’s up to the implementation class to decide to support features based on their desired feature. They can use their respective interface.
Interface Segregation Principle
“Clients should not be forced to add the Interfaces which are not required according to their proposed module. Instead of accumulating functions in one interface, we must create many small groups of functions into different interfaces, each one serving one sub-module.”
For example, let’s say we have an interface called UPIPayment like
Now, let’s have a look at a few implementations of UPIPayment, like Google Pay and Paytm.
Consider that Google Pay supports all the features so we can directly implement the UPIPayment, but Paytm doesn’t support the get CashbackAsCredit() feature, so here we shouldn’t force the client to add the Cashback Component as their feature.
We should create a separate interface that will deal with Cashback components.
So, we can now remove the getCashbackAsCredit() method from UPIPayment Interface.
In Conclusion, we should practice segregating the interface based on the features according to the client’s requirement, and clients must not be forced to use anything.
Dependency Inversion Principle
The dependency Inversion Principle states two critical parts as follows:
High-level modules should not depend on low-level modules. Both must depend on abstractions (Interface). Abstractions must not depend on the details but vise-versa.
The high-level modules contain the important policy decisions and business models of any application. If these modules depend on Low-level details, any modification to the low-level modules can directly affect the high-level modules, forcing them to change in turn. When building a real-time application, we must always keep the high-level modules and low-level modules loosely coupled to each other as much as possible. It helps us to protect other classes while making a change to any specific class. All in all, high-level modules and low-level modules should depend on abstraction.
Let’s say we are visiting a Shopping Mall where we bought some items from the Store. At the Payment Desk, we all have different Payment Methods like Credit Card, Debit Card, U.P.I., or Cash where we need to choose any one of the Payment Methods. Let’s consider one case here, where the Customer wants to pay the bill from the Credit Card while the Shopping Mall Application only supports the Payment from the Debit Card.
Considering, Shopping Mall only supports Debit Card Payment Method. Shopping Mall Billing Manager won’t look at the Card Type or ask the Customer if the provided CardType is a Debit/Credit Card. They will just swipe the Card in the Machine.
Assuming Shopping Mall wants to switch from Debit Card Payment Type to Credit Card Payment Type. We now need to refactor so many lines of our code in the shopping mall class.
Instead, we can design the code like
Here, we can conclude that the Shopping Mall Client wanted to invert the Debit Card Payment to Credit Card Payment, and so by refactoring one line in code, we can achieve it. That is basically what the Dependency Inversion Principle states.
In this blog, we started with the history of S.O.L.I.D. Principles, and then we tried to acquire a clear knowledge of each principle with examples of implemented applications in Game Development. I recommend keeping these principles in mind while designing, writing, and refactoring your code so that your code will be much cleaner, extendable, and testable.
Thanks for Reading!