Introduction: Why SOLID is Important for a Java Developer
In the world of object-oriented programming, maintaining and extending code becomes increasingly complex over time. Without a clear architecture, even a small project turns into a "big ball of mud," where changing one function breaks ten others. This is where SOLID principles come to the rescue — five fundamental rules of class design formulated by Robert Martin (Uncle Bob).
For a Java developer, SOLID is not just theory, but a practical tool. Java, as a strongly typed language with a powerful system of interfaces and inheritance, is ideally suited for implementing these principles. In this article, we will break down each principle from a theoretical perspective and show live Java code examples that can be immediately applied in a project.
1. Single Responsibility Principle
Theory
SRP states: a class should have only one reason to change. In other words, a class should be responsible for one, clearly defined functionality. If a class does too much, it becomes difficult to test, modify, and reuse.
Practice in Java
Consider a typical SRP violation — a class that simultaneously manages user data and sends notifications:
// ❌ SRP Violationpublic class User { private String name; private String email;
public void saveToDatabase() { // Saving to DB }
public void sendWelcomeEmail() { // Sending email }}The correct solution is to separate the responsibility into two classes:
// ✅ SRP Compliancepublic class User { private String name; private String email; // Getters and setters}
public class UserRepository { public void save(User user) { // Saving to DB }}
public class EmailService { public void sendWelcomeEmail(User user) { // Sending email }}Conclusion: each class now has one reason to change. User changes only when the data structure changes, UserRepository changes when the DB logic changes, EmailService changes when email templates change.
2. Open-Closed Principle
Theory
OCP states: classes should be open for extension, but closed for modification. This means new functionality should be added through inheritance or composition, without touching already tested code.
Practice in Java
Imagine you have a discount calculator for different customer types:
// ❌ OCP Violationpublic class DiscountCalculator { public 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; }}When adding a new customer type (e.g., "Gold"), you would have to modify the existing class. The solution is to use polymorphism:
// ✅ OCP Compliancepublic interface DiscountStrategy { double calculate(double amount);}
public class RegularDiscount implements DiscountStrategy { @Override public double calculate(double amount) { return amount * 0.1; }}
public class PremiumDiscount implements DiscountStrategy { @Override public double calculate(double amount) { return amount * 0.2; }}
public class DiscountCalculator { private DiscountStrategy strategy;
public DiscountCalculator(DiscountStrategy strategy) { this.strategy = strategy; }
public double calculate(double amount) {