Home SOLID Principles in Java
Post
Cancel

SOLID Principles in Java

When developing software applications, it’s crucial to consider the foundational principles of good software design. These principles guide the way we write, structure, and maintain our code. This article focuses on Java, but the principles we discuss here are generally applicable to other languages as well.

The principles we’ll cover include:

  1. Single Responsibility Principle (SRP)
  2. Open-Closed Principle (OCP)
  3. Liskov Substitution Principle (LSP)
  4. Interface Segregation Principle (ISP)
  5. Dependency Inversion Principle (DIP)

These are often collectively known as SOLID principles. Let’s break them down and look at how they apply to Java code.

1. Single Responsibility Principle (SRP)

SRP dictates that a class should have only one reason to change. In other words, each class should have only one job or responsibility. This principle aims to enhance code readability and maintainability by separating concerns.

Let’s look at a simple Java code example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Employee {
    private String employeeName;
    private String employeeAddress;
    
    public void setEmployeeName(String employeeName){
        this.employeeName = employeeName;
    }
    
    public void setEmployeeAddress(String employeeAddress){
        this.employeeAddress = employeeAddress;
    }
    
    public void saveEmployeeDetails(){
        //save details to database
    }
}

In this example, the Employee class is responsible for both holding employee data and saving it to the database. This violates the SRP. We can refactor this code to follow the SRP as follows:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Employee {
    private String employeeName;
    private String employeeAddress;
    
    public void setEmployeeName(String employeeName){
        this.employeeName = employeeName;
    }
    
    public void setEmployeeAddress(String employeeAddress){
        this.employeeAddress = employeeAddress;
    }
}

public class EmployeeDatabase {
    public void saveEmployeeDetails(Employee employee){
        //save details to database
    }
}

After refactoring, the Employee class’s sole responsibility is to hold employee data, while the EmployeeDatabase class handles database operations.

2. Open-Closed Principle (OCP)

OCP states that software entities (classes, modules, functions, etc.) should be open for extension but closed for modification. This means you should be able to add new features or functionality without changing the existing code.

Here’s an example that violates the OCP:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Rectangle {
    public double length;
    public double width;
}

class Circle {
    public double radius;
}

class AreaCalculator {
    public double calculateArea(Object shape) {
        if (shape instanceof Rectangle) {
            Rectangle rectangle = (Rectangle) shape;
            return rectangle.length * rectangle.width;
        } else if (shape instanceof Circle) {
            Circle circle = (Circle) shape;
            return Math.PI * Math.pow(circle.radius, 2);
        }
        return 0;
    }
}

In the example above, every time we add a new shape, we have to modify the AreaCalculator class, which violates the OCP. Here’s a refactored version that follows the OCP:

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
interface Shape {
    double calculateArea();
}

class Rectangle implements Shape {
    public double length;
    public double width;

    @Override
    public double calculateArea() {
        return length * width;
    }
}

class Circle implements Shape {
    public double radius;

    @Override
    public double calculateArea() {
        return Math.PI * Math.pow(radius, 2);
    }
}

class AreaCalculator {
    public double calculateArea(Shape shape)

 {
        return shape.calculateArea();
    }
}

3. Liskov Substitution Principle (LSP)

LSP states that subclasses must be substitutable for their base classes without causing issues. In other words, if class B is a subclass of class A, we should be able to replace A with B without disrupting the behavior of our program.

Consider the following example:

1
2
3
4
5
6
7
8
9
10
11
12
class Bird {
    public void fly() {
        System.out.println("Bird is flying");
    }
}

class Ostrich extends Bird {
    @Override
    public void fly() {
        throw new UnsupportedOperationException("Ostrich can't fly");
    }
}

In the above code, even though an ostrich is technically a bird, it can’t fly. So, calling the fly() method on an Ostrich object would lead to a runtime error, which violates LSP. Here’s a better way to model this situation:

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
interface Bird {
    void eat();
}

interface FlyingBird extends Bird {
    void fly();
}

class Sparrow implements FlyingBird {
    @Override
    public void eat() {
        System.out.println("Sparrow is eating");
    }
    
    @Override
    public void fly() {
        System.out.println("Sparrow is flying");
    }
}

class Ostrich implements Bird {
    @Override
    public void eat() {
        System.out.println("Ostrich is eating");
    }
}

4. Interface Segregation Principle (ISP)

ISP advises that clients should not be forced to depend on interfaces they do not use. Essentially, it’s better to have many small, specific interfaces than one large, general interface.

Here’s an example that violates ISP:

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
interface Worker {
    void work();
    void eat();
}

class WorkerImpl implements Worker {
    @Override
    public void work() {
        // work
    }
    
    @Override
    public void eat() {
        // eat
    }
}

class Robot implements Worker {
    @Override
    public void work() {
        // work
    }
    
    @Override
    public void eat() {
        throw new UnsupportedOperationException("Robot can't eat");
    }
}

In the above code, the Robot class is forced to implement an eat() method that it doesn’t need, which violates the ISP. We can fix this by segregating the interface:

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
interface Worker {
    void work();
}

interface Eater {
    void eat();
}

class WorkerImpl implements Worker, Eater {
    @Override
    public void work() {
        // work
    }
    
    @Override
    public void eat() {
        // eat
    }
}

class Robot implements Worker {
    @Override
    public void work() {
        // work
    }
}

5. Dependency Inversion Principle (DIP)

DIP states that high-level modules should not depend on low-level modules. Both should depend on abstractions. Additionally, abstractions should not depend on details. Details should depend on abstractions.

Consider this code:

1
2
3
4
5
6
7
8
9
10
11
12
13
class EmailService {
    void sendEmail(String message, String receiver){
        // logic to send email
    }
}

class Notification {
    private EmailService emailService = new EmailService();

    void promotionalNotification(String message, String receiver) {
        this.emailService.sendEmail(message, receiver);
    }
}

The Notification class is highly dependent on the EmailService class. This is a violation of the DIP. Instead, we should depend on an abstraction, like so:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
interface MessageService {
    void sendMessage(String message, String receiver);
}

class EmailService

 implements MessageService {
    @Override
    public void sendMessage(String message, String receiver) {
        // logic to send email
    }
}

class Notification {
    private MessageService messageService;

    Notification(MessageService messageService) {
        this.messageService = messageService;
    }

    void promotionalNotification(String message, String receiver) {
        this.messageService.sendMessage(message, receiver);
    }
}

In the refactored code, both Notification and EmailService depend on the MessageService interface, an abstraction. This allows us to easily substitute EmailService with another service if needed.

Wrapping Up

By adhering to these SOLID principles, you can write software that’s easy to maintain, understand, and expand. While it might require a bit of extra effort up front, it’s well worth it in the long run. Remember, these principles aren’t hard rules, but rather guidelines to help you structure your code effectively. Use them wisely to improve your Java codebase.

This post is licensed under CC BY 4.0 by the author.