Solid principles in java - What and how to implement these principles with example
In this blog, we will look at the solid principles of Java with examples. There are five solid principles of java.
What are solid principles java?
Solid principles are object-oriented design concepts.
- Single Responsibility Principle (SRP)
- Open Closed Principle (OCP)
- Liskov Substitution Principle (LSP)
- Interface Segregation Principle (ISP)
- Dependency Inversion Principle (DIP)
Single Responsibility Principle
Single responsibility principle (SRP) states that a class or module should perform only one task or have only one functionality, and so should have only one reason to change.
The class can have many properties or methods all related to the single functionality.
A class must change only for one reason. It means that each class must have one responsibility and if there is a change in that responsibility then we can change the code without affecting the existing code.
We all know requirements change over time. If one class has multiple functionality, it becomes difficult to change the class without affecting other classes, even those that may not have a direct impact on our changes.
In the end, you need to change your class more often. Each change is more complicated, has more side effects, and requires a lot more work than it should. So, it's better to avoid these problems by making sure that each class has only one responsibility.
If you see below, the Employee class has four methods each having different functionality. However, all are placed under one class. In the future, if we want to change the salary increment logic or database configuration then we have to make changes in the Employee class.
This is not good practice according to the Single Responsibility principle. Now we will create separate classes for creating reports and database operations, as well as salary calculation.
So each class will have only one responsibility.
public class Employee {
public void saveEmployeeToDatabase() {
//logic to save employee to database
};
public void createPDFReportOfEmployee() {
//creating report in pdf
};
public void createCSVReportOfEmployee() {
//creating report in csv
};
public void calculateSalaryIncrement() {
//logic to calculate salary increment
}
}
In this way, the class becomes easy to understand and maintain since the class has only one responsibility, making it easier to test the class.
Open Closed Principle
Open Closed Principle (OCP) states that a class or module should be open for extension and closed for modification.
The class should be open for any new functionality but closed for modifying the existing code. We should create a new class or method for new functionality rather than alter the old one.
The reason is that the old code is well tested and is already deployed in production. Due to this, if we make any changes to old code that is in production, our existing functionality may be affected, leading to bugs in the future. Also, each time we make changes we will be testing the entire functionality as we have touched old code.
For example, we need to modify the Payment class if we want to accept debit cards or paytm payments.
public class Payment {
public void doPayment(String paymentType) {
if (paymentType.equals("UPI")) {
//process payment in UPI
} else if (paymentType.equals("creditCard")) {
//process payment in UPI
}
}
}
We will see how we can modify the code according to the Open-Closed Principle (OCP) example.
Create an interface and a method for processing the payment.
interface PaymentType{
public void processPayment();
}
Now create classes for each payment and implement the Payment Type interface. In the future, if anyone wants to add another payment option, they can create a new class and implement the interface instead of changing the existing code.
class UPIPayment implements PaymentType {
public void processPayment() {
System.out.println("Processing payment in UPI");
}
}
class CreditCardPayment implements PaymentType {
public void processPayment() {
System.out.println("Processing payment using credit card");
}
}
class DebitCardPayment implements PaymentType {
public void processPayment() {
System.out.println("Processing payment using dedit card");
}
}
Our main class that accepts payments will now look like this.
public class Payment {
public void doPayment(PaymentType paymentType) {
if (paymentType == null) {
throw new RuntimeException("invalid payement");
}
paymentType.processPayment();
}
}
Liskov Substitution Principle
According to the Liskov Substitution Principle (LSP), objects of a superclass must be interchangeable with objects of its subclass without breaking the application. This principle defines how our class must be when we are inheriting it.
Parent classes should be substituted with their child class without altering or changing the program properties.
A child class must not throw a new exception which is not thrown in the parent class method. It must also not include new validation that is not part of the contract defined by the parent class.
abstract class Shape {
public abstract void draw();
public abstract double getArea();
}
class Square extends Shape{
double length;
@Override
public void draw() {
// TODO Auto-generated method stub
}
@Override
public double getArea() {
return length*length;
}
}
class Circle extends Shape{
double radius;
@Override
public void draw() {
// TODO Auto-generated method stub
}
@Override
public double getArea() {
return Math.PI*radius*radius;
}
}
ISP (Interface Segregation Principle)
ISP (Interface Segregation Principle) states that clients should not be forced to implement (interface) unnecessary methods that they do not use.
In general, we should not write a large interface with a lot of methods, it is better to break it up into smaller interfaces. In this way, the client is not forced to write code for unnecessary methods by implementing an interface.
In this way, the Interface follows the single responsibility principle.
In this interface, we have calculation operations containing basic and advanced math operations.
interface CalucationOperation {
public int addition(int n1, int n2);
public int subtraction(int n1, int n2);
public int multiplication(int n1, int n2);
public int division(int n1, int n2);
public int sin(double n);
public int cos(double n);
public int tan(double n);
public int log(double n);
}
A basic calculator does not need to have advanced math operations, so a class which implements the CalculationOperation interface is forced to provide logic for all methods which violates the Interface Segregation Principle (ISP).
If we split this interface into smaller ones, The class is not required to implement all the methods.
interface AdvanceCalucationOperation {
public int addition(int n1, int n2);
public int subtraction(int n1, int n2);
public int multiplication(int n1, int n2);
public int division(int n1, int n2);
}
interface BasicCalucationOperation {
public int sin(double n);
public int cos(double n);
public int tan(double n);
public int log(double n);
}