Sunday, February 09, 2025

SOLID Design Principles in Java

java,programming,software development,technology,design principle
Writing clean, maintainable, and scalable code is essential in software development. The SOLID principles, a set of five design principles that enhance object-oriented programming (OOP), are one of the most effective ways to achieve this.

While writing Java code, we should follow the SOLID principle to maintain a better software architecture. This principle reduces code complexity and makes our code more testable.

The five different principles are:
  • S: Single Responsibility Principle (SRP)
  • OOpen-Closed Principle (OCP)
  • LLiskov Substitution Principle (LSP)
  • IInterface Segregation Principle (ISP)
  • DDependency Inversion Principle (DIP)
SOLID Design Principles in Java
This blog post reviews the Single Responsibility Principle (SRP) and shows how it can be applied using Java examples.

Assume a restaurant is run by a single man. He is solely in charge of cooking, selling, shopping, cleaning, and accounting. This allows him to run his restaurant for a limited time. However, over time, he may close his business due to exhaustion and inefficiency.
SOLID Design Principles in Java
So, what will he do to save his business? He ought to recruit help and delegate various tasks to different people.

Single Responsibility Principle (SRP)

This is the first principle of the Solid Design Principles group. It tells us:
There should be no more than one reason for a class to change.
The statement may appear a little complicated. However, this is not the case. A single responsibility indicates that a class should provide or address a specific functionality. A class with multiple responsibilities is subject to several changes.

Consider an image of a neatly organized toolbox. Each tool in this toolbox serves a specific purpose. A wrench is used for tightening bolts, and a hammer for hammering nails. Similarly, each class should have a single, well-defined function in software design.
SOLID Principles In Java

Let us look at a simple Java example:
public class UserService {

	// REGISTER A NEW USER
	public String register(UserDetails registerDetails) {
		System.out.println("User registration is completed successfully.");
		return "SUCCESS";
	}

	// UPDATE USER DETAILS
	public String updateUserDetails(UserDetails details) {
		System.out.println("User details is updated successfully.");
		return "SUCCESS";
	}

	// LOGIN USING USERNAME AND PASSWORD
	public String login(String userName, String password) {
		System.out.println("User credential is verified successfully.");
		return "SUCCESS";
	}

	// SEND OTP TO USER PHONE NUMBER
	public String sendOTP(String phoneNumber) {
		// CONSUME THIRD PARTY API TO SEND OTP VIA SMS
		// ..

		System.out.println("OTP is successfully sent.");
		return "SUCCESS";
	}

	// OTP VALIDATION
	public String validateOTP(String code) {
		// OTP/CODE VALIDATION LOGIC
		// ..

		System.out.println("OTP validation is completed successfully.");
		return "SUCCESS";
	}

	// SSO AUTHENTICATION: TOKEN VALIDATION
	public String validateOAuthToken(String token) {
		// CONSUME THIRD PARTY API TO VALIDATE OAUTH TOKEN
		// ..

		System.out.println("OAurh Token validation is completed successfully.");
		return "SUCCESS";
	}

	// SAVE AUDIT DETAILS - MAINTAIN A LOG FOR THE CHANGES OF USER DETAILS
	public String saveAudit(UserRegisterDetails details) {
		System.out.println("Audit information of user details is saved successfully.");
		return "SUCCESS";
	}
}
We can see that the UserService has methods for:
  • Register a new User
  • Update user details
  • User login using credentials
  • Send OTP to the user's phone number
  • OTP validation
  • OAuth token validation
  • Save audit information - changes in user details

We implemented several functionalities in this class. However, there are many reasons why this class may change. What are some possible reasons for the change in this UserService class?
  • If we add a new property to the UserDetails class, we must modify the register(...) and updateUserDetails(...) methods.
  • To send OTP, we must use third-party SMS API. If the logic for consumption in the SMS API changes, we should modify the sendOTP(...) method.
  • If the logic for OTP validation changes, we must update the validateOTP(..) method.
  • Assume we have implemented Office 365 SSO, which allows users to log in to our application using their Office 365 accounts. We now want users with Google and Github accounts to be able to log in to our application using SSO. So, once again, we must change the validateOAuthToken(...) method.
  • If we want to save audit information other than user details, we must modify the saveAudit(...) method.

So, as you can see, there are several reasons why our class will change. And that is what we should avoid. This is what the Single Responsibility Principle (SRP) says to avoid.

In the software development process, we can not avoid changes in our code. So, whenever there are changes, we should create a separate class or module to handle those responsibilities. This allows us to change our code in an organized manner.

So, when designing a class or module, remember that a class addresses a specific concern. If a change request is received for that class, there can only be one reason for it to change.

To follow the SRP, we divided the responsibilities of the UserService class into multiple classes.

For example, the UserRegistrationService is responsible for registering new user details:
public class UserRegistrationService {

    // REGISTER A NEW USER
    public String register(UserDetails registerDetails) {
        // LOGIC TO SAVE USER DETAILS IN DB
        // ...

        System.out.println("User registration is completed successfully.");
        return "SUCCESS";
    }
}

The UserDetailsUpdateService is responsible for updating existing user details:
public class UserDetailsUpdateService {

    // UPDATE USER DETAILS
    public String updateUserDetails(UserDetails details) {
        // LOGIC TO SAVE/UPDATE USER DETAILS IN DB
        // ...

        System.out.println("User details is updated successfully.");
        return "SUCCESS";
    }
}

In OTPService, we have methods to send and validate OTP.
public class OTPService {

	// SEND OTP TO USER PHONE NUMBER
	public String sendOTP(String phoneNumber) {
		// CONSUME THIRD PARTY API TO SEND OTP VIA SMS
		// ..

		System.out.println("OTP is successfully sent.");
		return "SUCCESS";
	}

	// OTP VALIDATION
	public String validateOTP(String code) {
		// CODE VALIDATION LOGIC
		// ..

		System.out.println("OTP validation is completed successfully.");
		return "SUCCESS";
	}
}

The AuditService class saves audit details.
public class AuditService {

    // SAVE AUDIT DETAILS - MAINTAIN A LOG FOR THE CHANGES IN THE ENTITIES
    public String saveAuditDetails(List<AuditDetails> auditDetails) {
        // LOGIC TO SAVE AUDIT DETAILS IN DB
        // ...

        System.out.println("Audit details saved successfully.");
        return "SUCCESS";
    }
}

Single Responsibility Principle (SRP)
Here we have our main class. The code snippet describes how to use the classes.
public class SingleResponsibilityMain {
    public static void main(String[] args) {

        // REGISTRATION SERVICE
        UserRegistrationService registrationService = new UserRegistrationService();
        registrationService.register(new UserDetails(0L,
                "Silas Ross",
                "silas_ross@gmail.com",
                "9090801010",
                "silas8090ross"));

        // UPDATE SERVICE
        UserDetailsUpdateService updateService = new UserDetailsUpdateService();
        updateService.updateUserDetails(new UserDetails(101L,
                "Silas Ross",
                "silas_6060_ross@gmail.com",
                "9090806060",
                "silas8090ross"));

        // LOG/AUDIT SERVICE
        AuditService auditService = new AuditService();
        auditService.saveAuditDetails(List.of(new AuditDetails(LocalDateTime.now(),
                "email",
                "silas_ross@gmail.com",
                "silas_6060_ross@gmail.com",
                "user",
                109L),
                new AuditDetails(LocalDateTime.now(),
                        "phone",
                        "9090801010",
                        "9090806060",
                        "user",
                        109L)));

        // OTP SERVICE
        OTPService otpService = new OTPService();
        otpService.sendOTP("9000000010");
        otpService.validateOTP("983417");
    }
}
Here you can see that we tested each class separately.

Using the Single Responsibility Principle, we divided a class's responsibilities or functionalities. This makes our code more modular, flexible, and maintainable. We can also test each class individually. Each class now has only one reason to change, as per the Single Responsibility Principle.

Happy coding!!! 😊
in

No comments:

Post a Comment

Popular posts