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)
- O: Open-Closed Principle (OCP)
- L: Liskov Substitution Principle (LSP)
- I: Interface Segregation Principle (ISP)
- D: Dependency Inversion Principle (DIP)
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.
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.
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";
}
}
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!!! 😊
No comments:
Post a Comment