SOLID Principles
SOLID is a set of 5 design principles that help you write:
-
clean code
-
flexible code
-
maintainable code
-
scalable code
They are guidelines, not rules.
Following them makes your OOP designs robust and easy to change.
S – Single Responsibility Principle
O – Open/Closed Principle
L – Liskov Substitution Principle
I – Interface Segregation Principle
D – Dependency Inversion Principle
1. Single Responsibility Principle (SRP)¶
Definition¶
A class should have only one reason to change.
In simple words:
-
One class = one job
-
A class should do only one thing
❌ Problem (Violating SRP)¶
class Student {
String name;
int marks;
void calculateGrade() {
// grading logic
}
void saveToDatabase() {
// database code
}
void printReport() {
// printing logic
}
}
Why this is bad?¶
This class has multiple responsibilities:
-
business logic (grade)
-
database logic
-
presentation logic
If database changes, this class changes
If report format changes, this class changes
❌ Too many reasons to change.
✅ Solution (Following SRP)¶
class Student {
String name;
int marks;
}
class GradeCalculator {
void calculateGrade(Student student) {
// grading logic
}
}
class StudentRepository {
void save(Student student) {
// database logic
}
}
class StudentReport {
void print(Student student) {
// printing logic
}
}
Benefits¶
-
Easier to maintain
-
Easier to test
-
Changes don’t break unrelated code
2. Open/Closed Principle (OCP)¶
Definition¶
Software entities should be open for extension but closed for modification.
Meaning:
-
You should add new behavior
-
Without changing existing code
❌ Problem (Violating OCP)¶
class DiscountCalculator {
double calculateDiscount(String customerType, double amount) {
if (customerType.equals("Regular")) {
return amount * 0.1;
} else if (customerType.equals("Premium")) {
return amount * 0.2;
}
return 0;
}
}
Why this is bad?¶
-
Adding a new customer type → modify the class
-
Risk of breaking existing logic
✅ Solution (Using Polymorphism)¶
interface Discount {
double calculate(double amount);
}
class RegularCustomerDiscount implements Discount {
public double calculate(double amount) {
return amount * 0.1;
}
}
class PremiumCustomerDiscount implements Discount {
public double calculate(double amount) {
return amount * 0.2;
}
}
class DiscountCalculator {
double calculateDiscount(Discount discount, double amount) {
return discount.calculate(amount);
}
}
Benefits¶
-
New discount types → new classes only
-
Existing code remains untouched
3. Liskov Substitution Principle (LSP)¶
Definition¶
Objects of a superclass should be replaceable with objects of its subclass without breaking the program.
Simple meaning:
-
Child class should behave like parent
-
No unexpected behavior
❌ Classic Violation Example¶
class Bird {
void fly() {
System.out.println("Bird flying");
}
}
class Ostrich extends Bird {
void fly() {
throw new UnsupportedOperationException();
}
}
Why this violates LSP?¶
-
Ostrich is a Bird
-
But Ostrich cannot fly
-
Replacing Bird with Ostrich breaks code
✅ Solution (Proper Abstraction)¶
interface Bird {
}
interface FlyingBird extends Bird {
void fly();
}
class Sparrow implements FlyingBird {
public void fly() {
System.out.println("Sparrow flying");
}
}
class Ostrich implements Bird {
// no fly method
}
Benefits¶
-
Correct inheritance
-
No runtime surprises
-
Safer polymorphism
4. Interface Segregation Principle (ISP)¶
Definition¶
Clients should not be forced to depend on interfaces they do not use.
Meaning:
-
Prefer many small interfaces
-
Avoid fat interfaces
❌ Problem (Violating ISP)¶
interface Machine {
void print();
void scan();
void fax();
}
class OldPrinter implements Machine {
public void print() {
System.out.println("Printing");
}
public void scan() {
// not supported
}
public void fax() {
// not supported
}
}
Why this is bad?¶
-
OldPrinter is forced to implement methods it doesn’t need
-
Leads to empty or exception code
✅ Solution (Split Interfaces)¶
interface Printer {
void print();
}
interface Scanner {
void scan();
}
interface Fax {
void fax();
}
class OldPrinter implements Printer {
public void print() {
System.out.println("Printing");
}
}
class MultiFunctionPrinter implements Printer, Scanner, Fax {
public void print() {}
public void scan() {}
public void fax() {}
}
Benefits¶
-
Cleaner code
-
No unused methods
-
Better flexibility
5. Dependency Inversion Principle (DIP)¶
Definition¶
High-level modules should not depend on low-level modules. Both should depend on abstractions.
In short:
- Depend on interfaces, not concrete classes
❌ Problem (Tight Coupling)¶
class MySQLDatabase {
void connect() {
System.out.println("Connected to MySQL");
}
}
class UserService {
MySQLDatabase db = new MySQLDatabase();
void saveUser() {
db.connect();
}
}
Why this is bad?¶
-
UserService is tightly coupled to MySQL
-
Switching to PostgreSQL requires code changes
✅ Solution (Using Abstraction)¶
interface Database {
void connect();
}
class MySQLDatabase implements Database {
public void connect() {
System.out.println("Connected to MySQL");
}
}
class PostgreSQLDatabase implements Database {
public void connect() {
System.out.println("Connected to PostgreSQL");
}
}
class UserService {
private Database database;
UserService(Database database) {
this.database = database;
}
void saveUser() {
database.connect();
}
}
Benefits¶
-
Loose coupling
-
Easy to swap implementations
-
Better testability (mocking)
One-Line Interview Summary¶
-
SRP: One class, one responsibility
-
OCP: Extend behavior without modifying code
-
LSP: Child should safely replace parent
-
ISP: Many small interfaces are better
-
DIP: Depend on abstractions, not implementations