Are You SOLID? Mastering the 5 Principles of Clean Code
Why SOLID principles?
The SOLID Principles are the first and most crucial step in mastering Low-Level Design. Most design interviews begin with foundational questions related to these principles because they allow interviewers to analyze a candidate's seriousness and determination in software design.
So, here's my suggestion: don't just memorize definitions. Instead, start absorbing the actual concepts. True understanding of these principles is the essence of great software.
What are SOLID principles?
SOLID principles are five object-oriented design principles, that help developers write software that is easier to understand, maintain, and extend.
S - Single Responsibility Principle
O - Open / Close Principle
L - Liskov Substitution Principle
I - Interface Segregation Principle
D - Dependency Inversion Principle
1. Single Responsibility Principle:
A class should have only one reason to change. In other words, a class should only have one job, one responsibility, and one purpose.
If a class takes more than one responsibility, it becomes coupled. This means that if one responsibility changes, the other responsibilities may also be affected, leading to a ripple effect of changes throughout the codebase.
Let's understand through a real time scenario.
The Bank Analogy:
Imagine a traditional bank where there's a different counter for each task: one for deposits, another for withdrawals, a third for loans, and so on. Each counter has a single responsibility, which makes the entire process fast and efficient. If there's an issue with a loan application, the manager can go directly to the loan counter to debug and solve the problem.
Now, imagine if there was only a single counter handling all tasks—deposits, withdrawals, and loans. That counter would be incredibly slow, error-prone, and a nightmare to manage. If a loan issue comes up, a person would have to sort through all the deposit and withdrawal paperwork just to find the issue.
This is exactly how the SRP works in software. By breaking down a large, complicated class into smaller, single-purpose classes, you make your code easier to debug, more efficient, and far more maintainable.
In simple terms, the SRP teaches us that a well-designed class does one thing and does it well. It's the first step towards writing code that is easy to manage, debug, and reuse.
2. Open / Close Principle:
Imagine you're a traveler from India, and you've brought your favorite phone charger with you. You arrive in the US, but you quickly find a problem: your Indian charger's plug isn't fitting into the US wall socket. You can't change your charger, and you certainly can't modify the hotel's electrical outlet. Both are "closed for modification."
The simple solution is to buy a travel adapter. You plug the adapter into the wall, and your Indian charger into the adapter. Your system (the charger) has now been extended to work in a new environment, and you didn't have to change a single thing about it.
This is the essence of OCP. By using an adapter, you've created a flexible system that can easily handle new requirements without causing a ripple effect of changes. It's a key principle for building software that can grow and evolve gracefully.
In a technical context, the Open/Closed Principle is achieved through abstractions. By programming to an interface, you ensure that new functionality can be added by creating a new concrete class that implements that interface. This practice keeps your existing code "closed" and untouched, making your entire codebase more stable and less prone to regression bugs every time a new feature is added.
3. Liskov Substitution Principle (LSP)
The Liskov Substitution Principle states that subtypes must be substitutable for their base types without altering the correctness of the program. This means that a child class should be able to step in for its parent class and work perfectly. If the child breaks the expected behavior of the parent, it violates LSP.
Let's make this tricky principle more "connective" with an example.
The Master Chef Analogy:
Imagine a world-famous chef who is hired by a huge catering company. The company has a custom "Dish-Making Machine" that has been built to work with this chef's exact cooking process. All the machine knows is that the chef has a method called prepare_dish(ingredients, recipe).
Now, the famous chef trains two students.
Violating LSP (The "Shortcut Chef"): One student, the "Shortcut Chef," tries to be clever. He creates his own
prepare_dish()method but secretly requires a third argument: a specialmagic_spicethat the company's Dish-Making Machine doesn't have. When the company tries to use him as a substitute for the famous chef, the machine breaks down. The "Shortcut Chef" promised to be a substitute but broke the machine's expected behavior by requiring an extra argument.Following LSP (The "Indian Cuisine Chef"): Another student, the "Indian Cuisine Chef," also has a
prepare_dish()method. He adds his own unique spices and techniques, giving the dish a completely different flavor. But importantly, he still uses the same inputs as the famous chef, and his method never causes the Dish-Making Machine to fail. The company can swap the famous chef with the Indian Cuisine Chef at any time, and the machine continues to work flawlessly.
This is exactly what LSP is about. It ensures that when you create new child classes, they won't cause unexpected problems for the rest of your system.
4. Interface Segregation Principle (ISP):
It says: "Don't force a class to depend on methods it does not use."
The Interface Segregation Principle states that no client should be forced to depend on methods it does not use. In simpler terms, this means that we should prefer many small, specific interfaces over one large, "fat" one.
Let's understand this with a real-time scenario.
The Domino's App Analogy:
Imagine the Domino's app. This single app is used by two very different types of users: a Customer and a Delivery Driver.
Now, imagine if both the customer and the delivery driver had to use a single, massive interface with every possible function. The customer would see functions like getDriverLocation(), updateDeliveryStatus(), and acceptPayment(), which are completely irrelevant to them. This would be confusing, frustrating, and a prime example of a "fat interface."
Domino's follows ISP. The customer's version of the app only shows the functions they need: selectPizza(), placeOrder(), and trackOrder(). The delivery driver's version, on the other hand, shows the functions they need: acceptDelivery(), updateLocation(), and markAsDelivered().
By segregating the large interface into smaller, more specific ones, the Domino's app becomes easy to use, and each user's experience is tailored to their specific needs.
5. Dependency Inversion Principle (DIP):
To understand the Dependency Inversion Principle, let's first clarify three key concepts:
High-Level Modules: These are the "brains" of your application. They contain the core logic and make big decisions, coordinating how different features work together.
Low-Level Modules: These are the "workhorses" that handle the detailed, specific tasks, like talking to a database, making API calls, or reading files.
Abstraction (Refer here)
The Dependency Inversion Principle states that High-Level Modules should not depend on Low-Level Modules. Both should depend on abstractions.
In simpler terms, rather than high-level classes controlling and depending on the details of lower-level ones, both should rely on an interface or an abstract class. This makes your code flexible, testable, and easier to maintain.




Comments
Post a Comment