SOLID Principles in Java: A Comprehensive Guide
The SOLID principles are a set of five design principles that help developers create more maintainable, flexible, and scalable software. These principles were introduced by Robert C. Martin (Uncle Bob) and have become fundamental guidelines for object-oriented software design.
What are SOLID Principles?
SOLID is an acronym where each letter represents a principle:
- S: Single Responsibility Principle (SRP)
- O: Open/Closed Principle (OCP)
- L: Liskov Substitution Principle (LSP)
- I: Interface Segregation Principle (ISP)
- D: Dependency Inversion Principle (DIP)
Let’s explore each principle with practical Java examples.
Single Responsibility Principle (SRP)
A class should have only one reason to change.
This principle states that a class should be responsible for a single task or functionality.
Bad Example:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
| public class UserService {
public void registerUser(User user) {
// Validate user
if (user.getEmail() == null || !user.getEmail().contains("@")) {
throw new IllegalArgumentException("Invalid email");
}
// Save user to database
String sql = "INSERT INTO users (name, email) VALUES (?, ?)";
try (Connection conn = DriverManager.getConnection("jdbc:mysql://localhost/mydb", "user", "password");
PreparedStatement stmt = conn.prepareStatement(sql)) {
stmt.setString(1, user.getName());
stmt.setString(2, user.getEmail());
stmt.executeUpdate();
} catch (SQLException e) {
throw new RuntimeException(e);
}
// Send confirmation email
sendEmail(user.getEmail(), "Welcome", "Welcome to our platform!");
}
private void sendEmail(String to, String subject, String body) {
// Code to send email
System.out.println("Email sent to " + to);
}
}
|
Good Example:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
| public class UserValidator {
public void validate(User user) {
if (user.getEmail() == null || !user.getEmail().contains("@")) {
throw new IllegalArgumentException("Invalid email");
}
}
}
public class UserRepository {
public void save(User user) {
String sql = "INSERT INTO users (name, email) VALUES (?, ?)";
try (Connection conn = DriverManager.getConnection("jdbc:mysql://localhost/mydb", "user", "password");
PreparedStatement stmt = conn.prepareStatement(sql)) {
stmt.setString(1, user.getName());
stmt.setString(2, user.getEmail());
stmt.executeUpdate();
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
}
public class EmailService {
public void sendWelcomeEmail(User user) {
sendEmail(user.getEmail(), "Welcome", "Welcome to our platform!");
}
private void sendEmail(String to, String subject, String body) {
// Code to send email
System.out.println("Email sent to " + to);
}
}
public class UserService {
private final UserValidator validator;
private final UserRepository repository;
private final EmailService emailService;
public UserService(UserValidator validator, UserRepository repository, EmailService emailService) {
this.validator = validator;
this.repository = repository;
this.emailService = emailService;
}
public void registerUser(User user) {
validator.validate(user);
repository.save(user);
emailService.sendWelcomeEmail(user);
}
}
|
Open/Closed Principle (OCP)
Software entities should be open for extension but closed for modification.
This principle emphasizes that you should be able to extend a class’s behavior without modifying it.
Bad Example:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
| public class Rectangle {
private double width;
private double height;
// Getters and setters
public double area() {
return width * height;
}
}
public class Circle {
private double radius;
// Getters and setters
public double area() {
return Math.PI * radius * radius;
}
}
public class AreaCalculator {
public double calculateArea(Object shape) {
if (shape instanceof Rectangle) {
Rectangle rectangle = (Rectangle) shape;
return rectangle.area();
} else if (shape instanceof Circle) {
Circle circle = (Circle) shape;
return circle.area();
}
throw new IllegalArgumentException("Unsupported shape");
}
}
|
Good Example:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
| public interface Shape {
double area();
}
public class Rectangle implements Shape {
private double width;
private double height;
// Getters and setters
@Override
public double area() {
return width * height;
}
}
public class Circle implements Shape {
private double radius;
// Getters and setters
@Override
public double area() {
return Math.PI * radius * radius;
}
}
public class Triangle implements Shape {
private double base;
private double height;
// Getters and setters
@Override
public double area() {
return 0.5 * base * height;
}
}
public class AreaCalculator {
public double calculateArea(Shape shape) {
return shape.area();
}
}
|
Liskov Substitution Principle (LSP)
Objects of a superclass should be replaceable with objects of its subclasses without affecting the correctness of the program.
This principle is about ensuring that a derived class can stand in for its base class without causing issues.
Bad Example:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
| public class Rectangle {
protected double width;
protected double height;
public void setWidth(double width) {
this.width = width;
}
public void setHeight(double height) {
this.height = height;
}
public double getWidth() {
return width;
}
public double getHeight() {
return height;
}
public double area() {
return width * height;
}
}
public class Square extends Rectangle {
@Override
public void setWidth(double width) {
super.setWidth(width);
super.setHeight(width);
}
@Override
public void setHeight(double height) {
super.setHeight(height);
super.setWidth(height);
}
}
// Client code
public void testRectangle(Rectangle rectangle) {
rectangle.setWidth(5);
rectangle.setHeight(4);
assert rectangle.area() == 20; // This fails for Square
}
|
Good Example:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
| public interface Shape {
double area();
}
public class Rectangle implements Shape {
private double width;
private double height;
public Rectangle(double width, double height) {
this.width = width;
this.height = height;
}
public double getWidth() {
return width;
}
public double getHeight() {
return height;
}
@Override
public double area() {
return width * height;
}
}
public class Square implements Shape {
private double side;
public Square(double side) {
this.side = side;
}
public double getSide() {
return side;
}
@Override
public double area() {
return side * side;
}
}
|
Interface Segregation Principle (ISP)
Clients should not be forced to depend on methods they do not use.
This principle advises creating specific interfaces rather than one general-purpose interface.
Bad Example:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
| public interface Worker {
void work();
void eat();
void sleep();
}
public class Robot implements Worker {
@Override
public void work() {
System.out.println("Robot is working");
}
@Override
public void eat() {
// Robots don't eat, but we're forced to implement this method
throw new UnsupportedOperationException("Robots don't eat");
}
@Override
public void sleep() {
// Robots don't sleep, but we're forced to implement this method
throw new UnsupportedOperationException("Robots don't sleep");
}
}
|
Good Example:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
| public interface Workable {
void work();
}
public interface Eatable {
void eat();
}
public interface Sleepable {
void sleep();
}
public class Human implements Workable, Eatable, Sleepable {
@Override
public void work() {
System.out.println("Human is working");
}
@Override
public void eat() {
System.out.println("Human is eating");
}
@Override
public void sleep() {
System.out.println("Human is sleeping");
}
}
public class Robot implements Workable {
@Override
public void work() {
System.out.println("Robot is working");
}
}
|
Dependency Inversion Principle (DIP)
High-level modules should not depend on low-level modules. Both should depend on abstractions.
Abstractions should not depend on details. Details should depend on abstractions.
This principle is about decoupling modules through abstractions.
Bad Example:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| public class EmailNotifier {
public void sendEmail(String to, String subject, String body) {
// Code to send email
System.out.println("Email sent to " + to);
}
}
public class UserRegistrationService {
private EmailNotifier emailNotifier = new EmailNotifier();
public void registerUser(User user) {
// Save user to database
// Send notification
emailNotifier.sendEmail(user.getEmail(), "Welcome", "Welcome to our platform!");
}
}
|
Good Example:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
| public interface NotificationService {
void sendNotification(String recipient, String subject, String body);
}
public class EmailNotifier implements NotificationService {
@Override
public void sendNotification(String recipient, String subject, String body) {
// Code to send email
System.out.println("Email sent to " + recipient);
}
}
public class SMSNotifier implements NotificationService {
@Override
public void sendNotification(String recipient, String subject, String body) {
// Code to send SMS
System.out.println("SMS sent to " + recipient);
}
}
public class UserRegistrationService {
private final NotificationService notificationService;
public UserRegistrationService(NotificationService notificationService) {
this.notificationService = notificationService;
}
public void registerUser(User user) {
// Save user to database
// Send notification
notificationService.sendNotification(user.getEmail(), "Welcome", "Welcome to our platform!");
}
}
|
Conclusion
The SOLID principles provide a framework for writing maintainable and scalable code. When applied correctly, they help to:
- Create more reusable code
- Reduce technical debt
- Make the codebase more robust to changes
- Improve readability and understanding of the code
- Make testing easier
While these principles might seem abstract at first, with practice, they become invaluable tools in a developer’s arsenal for creating high-quality software.
In future posts, we’ll dive deeper into design patterns that build upon these SOLID principles to solve common software design challenges.