avatarVinotech

Summary

The provided content discusses the application of SOLID principles in Java, particularly within the context of Spring Boot applications, to enhance software design and maintainability.

Abstract

The SOLID principles, a set of five design principles, are crucial for creating flexible, extensible, and understandable software in object-oriented programming. These principles include the Single Responsibility Principle (SRP), Open/Closed Principle (OCP), Liskov Substitution Principle (LSP), Interface Segregation Principle (ISP), and Dependency Inversion Principle (DIP). The article illustrates each principle with examples in Spring Boot, demonstrating how adherence to these principles can lead to better dependency management, code flexibility, and system robustness. By refactoring code to follow SOLID principles, developers can avoid common design pitfalls, improve code reusability, and ensure that their applications are well-equipped to handle the complexities of modern software development.

Opinions

  • The author emphasizes the importance of SOLID principles in improving software design and reducing the likelihood of poor design choices.
  • The article suggests that following SOLID principles leads to code that is easier to understand, maintain, and extend.
  • It is conveyed that SOLID principles, especially when applied in a Spring Boot context, contribute to building effective, flexible, maintainable, and agile software.
  • The author advocates for the use of interfaces and dependency injection in Spring Boot to adhere to the Dependency Inversion Principle, thereby decoupling high-level modules from low-level details.
  • The examples provided show a clear preference for modular design, where classes and interfaces have distinct, well-defined responsibilities.
  • The article implies that proper application of SOLID principles can significantly reduce the fragility, rigidity, and immobility of code, which are characteristics of bad design.

SOLID Principles in Java

If you like this post, Please clap for it.

SOLID principles and design patterns are object-oriented approaches used in software development, intended to make software design more flexible, extensible, and understandable. The SOLID principles were first introduced by American software engineer and instructor Robert C.

In the software development lifecycle, accessibility and flexibility of an object are very important in an object’s design phase to ensure its simplicity, maintenance, ease of implementation, and accessibility towards making good software.

Importance of SOLID principles:

SOLID principles are general guidelines to design a software system if followed properly can reduce the chance of having bad designs. Robert Martin in his article Principles of OOD has listed some important aspects of bad design that should be avoided. He talked about how poor dependency management can lead to code that is rigid, fragile, and immobile. On the other hand, well-managed dependencies can lead to code that is well-maintained, flexible and reusable. SOLID principles expose developers to the dependency management aspects of Object-oriented design. They focus very much on dependency management. Find below the importance of using SOLID principles.

  1. SOLID principles are general principles that every developer should know to develop software properly to avoid design smells.
  2. The main focus is to improve dependency management to make the code flexible, understandable, and reusable.
  3. SOLID principles, if followed properly, make the designs easier to understand, maintain and extend.
  4. These design patterns help the developers to avoid design issues and to build effective, flexible, maintainable, and agile software.

SOLID stands for below five design principles:

  1. S — Single Responsibility Principle(SRP)
  2. O — Open Closed Principle(OCP)
  3. L — Liskov Substitution Principle(LSP)
  4. I — Interface Segregation Principle(ISP)
  5. D — Dependency Inversion Principle(DIP)

S — Single Responsibility Principle (SRP)

The Single Responsibility Principle tells us that a class should have only one responsibility and only one reason to change.

Example in Spring Boot Consider a Spring Boot application in which there is a service class dealing with user management along with notification logic. This has violated SRP since the class now has more than one reason to change, as in user management logic changes or notification logic.

Before (violating SRP)

import org.springframework.stereotype.Service;

@Service
public class UserNotificationService {

    public void addUser(String userName) {
        // Add user logic
    }

    public void sendEmailNotification(String email) {
        // Email notification logic
    }
}

After (adhering to SRP)

We should refactor this into two separate classes, each focusing on a single responsibility.

import org.springframework.stereotype.Service;

@Service
public class UserService {

    public void addUser(String userName) {
        // Add user logic
    }
}

@Service
public class NotificationService {

    public void sendEmailNotification(String email) {
        // Email notification logic
    }
}

In the refactored version, UserService manages user-related operations, while NotificationService takes care of sending notifications. This adheres to SRP, making the Spring Boot application more modular, easier to understand, and maintain.

Next, we’ll delve into the Open/Closed Principle (OCP) with an example tailored for Spring Boot.

O — Open/Closed Principle (OCP)

The Open/Closed Principle stipulates that any software entity, class, module, function, etc., should be open for extension but closed for modification. It says that we should be able to add new functionality to an existing object without altering its existing behavior.

Example in Spring Boot In a Spring Boot application, we can demonstrate OCP that allows the extension of classes, in order for them to show polymorphic behavior, without actually having to do any changes to a given class.

Before (potentially violating OCP)

Suppose we have a service that processes messages, but it can only handle text messages.

import org.springframework.stereotype.Service;

@Service
public class MessageService {
    public void processMessage(String message) {
        // Process text message
        System.out.println("Processing text message: " + message);
    }
}

After (adhering to OCP)

We introduce an interface and extend the functionality to handle different types of messages without modifying the original class.

import org.springframework.stereotype.Service;

public interface MessageProcessor {
    void processMessage(String message);
}
@Service
public class TextMessageProcessor implements MessageProcessor {
    @Override
    public void processMessage(String message) {
        System.out.println("Processing text message: " + message);
    }
}
@Service
public class ImageMessageProcessor implements MessageProcessor {
    @Override
    public void processMessage(String message) {
        // Logic for processing image message
        System.out.println("Processing image message: " + message);
    }
}

In this Spring Boot example, MessageProcessor is an interface that defines the processMessage method. TextMessageProcessor and ImageMessageProcessor are two implementations of this interface that handle text and image messages, respectively. This design adheres to the OCP, allowing the system to introduce new message types without modifying existing code.

Moving forward, we will explore the Liskov Substitution Principle (LSP) with a Spring Boot example.

L — Liskov Substitution Principle (LSP)

The Liskov Substitution Principle (LSP) states that a superclass object can and should be substitutable with any of its subclasses’ objects, such that the program would stop running correctly. This guarantees that any subclass can replace its superclass without bringing an error or surprise.

Example in Spring Boot

To demonstrate LSP in Spring Boot, let’s consider a scenario involving user roles and access permissions.

Before (potential LSP violation)

Imagine we have a base class for a User and a subclass AdminUser. If the subclass AdminUser changes the behavior of the base class in a way that is not expected, it could lead to LSP violations.

import org.springframework.stereotype.Component;

@Component
public class User {
    public String accessResource() {
        return "Accessed user resource";
    }
}
@Component
public class AdminUser extends User {
    @Override
    public String accessResource() {
        throw new RuntimeException("Admin cannot access this resource");
    }
}

In this example, AdminUser unexpectedly throws an exception when accessing a resource, which could cause issues if we expect an AdminUser to behave like a User.

After (adhering to LSP)

To adhere to LSP, subclasses should enhance, not replace or restrict, the behavior of their base classes.

import org.springframework.stereotype.Component;

@Component
public class User {
    public String accessResource() {
        return "Accessed user resource";
    }
}
@Component
public class AdminUser extends User {
    @Override
    public String accessResource() {
        return "Accessed admin resource"; // Extends the behavior, does not restrict it
    }
}

In the revised example, AdminUser extends the functionality of User by accessing admin-specific resources, ensuring that AdminUser can be used anywhere a User can, thus adhering to the LSP.

With this understanding of LSP in the context of Spring Boot, we can now move on to discuss the Interface Segregation Principle (ISP) with an example.

I — Interface Segregation Principle (ISP)

Interface Segregation Principle (ISP) suggests refining the big, general-purpose interface to become smaller, focused ones. The principle, as a matter of fact, would be very instrumental in seeing to it that the implementing classes do have methods that are only relevant for them, hence making classes very modular and of high cohesion.

Example in Spring Boot

To illustrate the ISP in a Spring Boot application, let’s consider a service interface with multiple responsibilities.

Before (violating ISP)

Here, we have an interface that combines different functionalities, which not all implementing classes will use.

import org.springframework.stereotype.Service;

public interface UserOperations {
    void addUser(String user);
    void updateUser(String user);
    void deleteUser(String user);
    void sendUserNotification(String user);
}
@Service
public class UserService implements UserOperations {
    @Override
    public void addUser(String user) {
        // Add user logic
    }
    @Override
    public void updateUser(String user) {
        // Update user logic
    }
    @Override
    public void deleteUser(String user) {
        // Delete user logic
    }
    @Override
    public void sendUserNotification(String user) {
        // Not applicable for basic user service
    }
}

After (adhering to ISP)

We can segregate the UserOperations interface into smaller, more specific interfaces.

import org.springframework.stereotype.Service;

public interface UserManagement {
    void addUser(String user);
    void updateUser(String user);
    void deleteUser(String user);
}
public interface UserNotification {
    void sendUserNotification(String user);
}
@Service
public class UserService implements UserManagement {
    
    @Override
    public void addUser(String user) {
        // Add user logic
    }
    @Override
    public void updateUser(String user) {
        // Update user logic
    }
    @Override
    public void deleteUser(String user) {
        // Delete user logic
    }
}
@Service
public class NotificationService implements UserNotification {
    @Override
    public void sendUserNotification(String user) {
        // Send notification logic
    }
}

In this refactored example, UserService implements UserManagement, handling user-related operations, and NotificationService implements UserNotification, dealing with sending notifications. This segregation ensures that each service class only implements the functionalities relevant to it, adhering to the ISP.

Finally, we will look at the Dependency Inversion Principle (DIP) with a Spring Boot example.

D — Dependency Inversion Principle (DIP)

The Dependency Inversion Principle (DIP) explains that high-level modules should not depend on the low-level modules, and it should be both depending upon abstractions. The abstractions should not depend on details; otherwise, the abstractions depend upon the details. One of the core principles allows one to design the system such that the policy at a high level is decoupled from low-level details, so individual parts could be replaced or changed.

Example in Spring Boot For example, in a Spring Boot scenario, this could take the form of defining interfaces (or abstractions) for the dependencies and then allowing them to be injected at runtime — normally, the Spring-provided dependency injection mechanism would aid in this.

Before (not fully adhering to DIP)

Here’s an example where a high-level class directly depends on a low-level class.

import org.springframework.stereotype.Component;

@Component
public class UserProcessor {
    private DatabaseService databaseService = new DatabaseService();
    public void processUser(String user) {
        databaseService.saveUser(user);
    }
}
@Component
public class DatabaseService {
    public void saveUser(String user) {
        // Save user to the database
    }
}

After (adhering to DIP)

To adhere to the DIP, we should depend on an abstraction rather than a concrete class.

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;

interface DatabaseService {
    void saveUser(String user);
}
@Service
public class DatabaseServiceImpl implements DatabaseService {
    
    @Override
    public void saveUser(String user) {
        // Save user to the database
    }
}
@Component
public class UserProcessor {
    private DatabaseService databaseService;
    @Autowired
    public UserProcessor(DatabaseService databaseService) {
        this.databaseService = databaseService;
    }
    public void processUser(String user) {
        databaseService.saveUser(user);
    }
}

In the improved example, UserProcessor depends on the DatabaseService abstraction, not on the concrete DatabaseServiceImpl. This way, DatabaseService can be easily replaced or modified without affecting UserProcessor, demonstrating the Dependency Inversion Principle. Spring Boot’s dependency injection mechanism (@Autowired) is used to inject the concrete implementation at runtime, allowing for greater flexibility and decoupling of components.

Conclusion

Thus, our brief dive into SOLID principles in the context of Spring Boot was demonstrating how SOLID principles can be applied during the development of Java applications in order to come up with more maintainable, scalable, and robust systems. By continuing to use these principles, developers make sure that their apps are well-designed and ready to stand up to all the complexities that face modern software development.

Solid Principles In Java
Java
Spring Boot
Design Patterns
Object Oriented
Recommended from ReadMedium