Hi all..๐
I studied SOLID principles today. I'll try to explain SOLID principles in the simplest way possible.
What are SOLID principles?
Solid is a set of 5 design principles that help us write cleaner and maintainable code.
SOLID stands for :
- S - Single Responsibility
- O - Open/Closed Principle
- L - Liskov Substitution
- I - Interface Segregation
- D - Dependency Inversion
S - Single Responsibility Principle
Single Responsibility Principle states that a class should have only one responsibility to handle.
Analogy :
A chef should focus on cooking, not taking orders, managing inventory, and handling payments. Everyone in a restaurant has a specific role.
The same idea applies to our code.
โBad Example:
In the example below, the EmployeeService class is doing too many things.
class EmployeeService {
public void saveEmployee(Employee employee) {
// save employee
}
public void generateReport() {
// generate report
}
public void sendEmail(Employee employee) {
// send email
}
}
As per the above mentioned example, the EmployeeService class handles too many responsibilites like managing employees. generate report and sending email.
โ
Good Example:
class EmployeeService {
public void saveEmployee(Employee employee) {
// save employee }
}
class ReportService {
public void generateReport() {
// generate report
}
}
class EmailService {
public void sendEmail(Employee employee) {
// send email
}
}
Now each class has a clear purpose.
Why is this Better?
Easier to understand
Changes in one feature are less likely to affect others
For example: if email functionality changes, we only need to modify the EmailService class without touching employee or reporting logic.
O - Open/Closed Principle
We should be able to add new features without changing code that already works.
Analogy:
A library can add new books without rebuilding the entire library.
โBad example:
Suppose we have a notification service.
class NotificationService {
public void sendNotification(String type) {
if (type.equals("EMAIL")) {
System.out.println("Sending Email");
} else if (type.equals("SMS")) {
System.out.println("Sending SMS");
}
}
}
Now if we need to add WhatsApp notifications support we will have to modify the existing class and add another condition.
else if (type.equals("WHATSAPP")) {
System.out.println("Sending WhatsApp Message");
}
Every new notification type requires changing existing code.
โ
Good example:
Create an interface that all notification types can follow.
interface Notification {
void send();
}
class EmailNotification implements Notification {
public void send() {
System.out.println("Sending Email");
}
}
class SmsNotification implements Notification {
public void send() {
System.out.println("Sending SMS");
}
}
Adding WhatsApp support becomes easy.
class WhatsAppNotification implements Notification {
public void send() {
System.out.println("Sending WhatsApp Message");
}
}
No existing classes need to be modified.
Why is this Better?
Existing code remains stable.
New features can be added easily.
Makes the application easier to extend.
L- Liskov Substitution
We should be able to replace the object of a subclass with the object of a parent class without changing anything in the program.
Analogy:
A hotel room accepts a key card. Replacing one valid key card with another should still open the door. A replacement should not change the expected behavior.
โBad Example:
class Bird {
void fly() {
System.out.println("Flying");
}
}
class Sparrow extends Bird {
void fly() {
System.out.println("Sparrow flying");
}
}
class Penguin extends Bird {
void fly() {
throw new RuntimeException("Penguin can't fly");
}
}
Penguin cannot fly.
Sparrow can be used in place of bird but Penguin cannot fly and this creates problems.
โ
Good example:
class Bird {
void fly() {
System.out.println("Sparrow flying");
}
}
class NonFlyingBird extends Bird {
void Swim() {
System.out.println("Swim");
}
}
class Sparrow extends Bird {
}
class Penguin extends NonFlyingBird {
}
Now:
Sparrow is a flying bird.
Penguin is a bird but not a flying bird.
No broken expectations.
I- Interface Segregation
Don't force a class to implement methods it doesn't need.
Analogy:
A person borrowing books should not be forced to borrow magazines as well.
One big interface forcing everything:
interface Restaurant {
void takeOrder();
void cookFood();
void serveFood();
void manageBilling();
}
Now different staff are forced to implement things they donโt need:
class Waiter implements Restaurant {
public void takeOrder() {
System.out.println("Taking order");
}
public void cookFood() {
// โ Not my job
}
public void serveFood() {
System.out.println("Serving food");
}
public void manageBilling() {
// โ Not my job
}
}
class Chef implements Restaurant {
public void takeOrder() {
// โ Not my job
}
public void cookFood() {
System.out.println("Cooking food");
}
public void serveFood() {
// โ Not my job
}
public void manageBilling() {
// โ Not my job
}
}
Problem:
Classes depend on methods they donโt use
Forced implementations
confusing design
โ
Good Example:
Split into focused interfaces:
interface OrderTaker {
void takeOrder();
}
interface Cook {
void cookFood();
}
interface Server {
void serveFood();
}
interface BillManager {
void manageBilling();
}
Now each role implements only what it needs:
class Waiter implements OrderTaker, Server {
public void takeOrder() {
System.out.println("Taking order");
}
public void serveFood() {
System.out.println("Serving food");
}
}
class Chef implements Cook {
public void cookFood() {
System.out.println("Cooking food");
}
}
class Cashier implements BillManager {
public void manageBilling() {
System.out.println("Handling billing");
}
}
result: No unnecessary methods and clean responsibilities.
D- Dependency Inversion
High-level modules should NOT depend on low-level modules.
Both should depend on abstractions (interfaces).
Analogy: A phone depends on USB-C port instead of a specific charger brand.
โBad example:
class DebitCard{
void pay(){
}
}
class CreditCard{
void pay(){
}
}
class Shop{
DebitCard card;
public Shop(DebitCard card) {
this.card = card;
}
void payment(DebitCard card){
card.pay();
}
}
Here Debit card is tightly coupled.
โ
Good example:
interface BankCard{
void pay();
}
class DebitCard implements BankCard{
public void pay(){
}
}
class CreditCard implements BankCard{
public void pay(){
}
}
class Shop{
BankCard card;
public Shop(BankCard card) {
this.card = card;
}
void payment(DebitCard card){
card.pay();
}
}
Why is this better?
Shop does not depend on concrete classes
We can add new cards without changing Shop
Easy to extend, easy to test
Loosely coupled design
Top comments (0)