Writing clean, scalable, and maintainable code is an essential skill for every software developer. The SOLID principles—coined by Robert C. Martin (aka Uncle Bob)—are five foundational design principles that help you build better object-oriented systems.
Let’s walk through each of them with practical Java examples and a clear explanation of what to avoid and how to fix it, inspired by best practices from sources like AlgoMaster and FreeCodeCamp.
🔹 1. Single Responsibility Principle (SRP)
💡 Definition:
A class should have only one reason to change. In simple terms, each class should focus on a single task or responsibility.
❌ Violation:
public class Report {
public String generateReport() {
// Generate report
return "Report content";
}
public void saveToFile(String report) {
// Save report to a file
}
}
Here, Report
is doing two jobs:
- Generating the report.
- Saving it.
So if saving logic changes (e.g., switch from file to database), we must change this class even if report generation stays the same.
✅ Solution:
Separate responsibilities into different classes:
public class ReportGenerator {
public String generateReport() {
return "Report content";
}
}
public class ReportSaver {
public void saveToFile(String report) {
// Save report to file
}
}
AlgoMaster Tip: Think of classes like tools: a screwdriver shouldn't also be a hammer.
🔹 2. Open/Closed Principle (OCP)
💡 Definition:
Software components should be open for extension, but closed for modification.
❌ Violation:
public class DiscountCalculator {
public double calculateDiscount(String customerType, double amount) {
if ("Regular".equals(customerType)) {
return amount * 0.1;
} else if ("Premium".equals(customerType)) {
return amount * 0.2;
}
return 0;
}
}
To add new customer types, you need to modify this method—violating OCP.
✅ Solution:
Use polymorphism and interfaces:
public interface Discount {
double calculate(double amount);
}
public class RegularCustomerDiscount implements Discount {
public double calculate(double amount) {
return amount * 0.1;
}
}
public class PremiumCustomerDiscount implements Discount {
public double calculate(double amount) {
return amount * 0.2;
}
}
Now you can extend discount logic without touching existing code.
FreeCodeCamp Insight: Extending behavior without modifying existing code makes software less fragile and more resilient to change.
🔹 3. Liskov Substitution Principle (LSP)
💡 Definition:
Subtypes must be substitutable for their base types without breaking functionality.
❌ Violation:
public class Bird {
public void fly() {
System.out.println("Bird is flying");
}
}
public class Sparrow extends Bird { }
public class Penguin extends Bird {
@Override
public void fly() {
throw new UnsupportedOperationException("Penguins can't fly");
}
}
Here, substituting Bird
with Penguin
causes unexpected behavior.
✅ Solution:
Separate behaviors using interfaces:
public interface Flyable {
void fly();
}
public class Bird { }
public class Sparrow extends Bird implements Flyable {
public void fly() {
System.out.println("Sparrow is flying");
}
}
public class Penguin extends Bird {
// No fly method since penguins don't fly
}
AlgoMaster Analogy: Don’t ask a fish to climb a tree. Define roles carefully.
🔹 4. Interface Segregation Principle (ISP)
💡 Definition:
Clients should not be forced to depend on methods they do not use.
❌ Violation:
public interface Worker {
void work();
void eat();
}
public class Robot implements Worker {
public void work() {
System.out.println("Robot working");
}
public void eat() {
throw new UnsupportedOperationException("Robots don't eat");
}
}
Robot has to implement eat()
even though it doesn't apply.
✅ Solution:
Split the interface:
public interface Workable {
void work();
}
public interface Eatable {
void eat();
}
public class Human implements Workable, Eatable {
public void work() { System.out.println("Human working"); }
public void eat() { System.out.println("Human eating"); }
}
public class Robot implements Workable {
public void work() { System.out.println("Robot working"); }
}
FreeCodeCamp Reminder: Favor focused interfaces. Small is beautiful.
🔹 5. Dependency Inversion Principle (DIP)
💡 Definition:
High-level modules should not depend on low-level modules. Both should depend on abstractions.
❌ Violation:
public class EmailService {
private final GmailService gmailService;
public EmailService() {
this.gmailService = new GmailService();
}
public void sendEmail(String message) {
gmailService.send(message);
}
}
public class GmailService {
public void send(String message) {
System.out.println("Sending via Gmail: " + message);
}
}
Here, EmailService
is tightly coupled to GmailService
. What if you want to use Outlook tomorrow?
✅ Solution:
Depend on abstraction:
public interface EmailProvider {
void send(String message);
}
public class GmailService implements EmailProvider {
public void send(String message) {
System.out.println("Sending via Gmail: " + message);
}
}
public class OutlookService implements EmailProvider {
public void send(String message) {
System.out.println("Sending via Outlook: " + message);
}
}
public class EmailService {
private final EmailProvider emailProvider;
public EmailService(EmailProvider emailProvider) {
this.emailProvider = emailProvider;
}
public void sendEmail(String message) {
emailProvider.send(message);
}
}
✅ This not only follows DIP but also supports OCP—you can switch providers without touching
EmailService
.
✅ Conclusion
The SOLID principles help you build code that is:
- 📦 Modular
- 🔧 Easy to maintain
- 🔁 Reusable
- ♻️ Testable
- 💪 Scalable
Whether you’re preparing for interviews, writing production-grade systems, or simply want to upskill, understanding and applying SOLID will elevate your object-oriented design skills.
📚 References & Inspirations:
- AlgoMaster Java Design Series
- FreeCodeCamp SOLID Principle Guide
Top comments (0)