SOLID Principles in Action: Writing Clean and Maintainable Java Code
The SOLID principle is a collection of guidelines that can be used by developers to create object-oriented code that is maintainable and scalable over time. These principles assist developers in writing code that is more fluid, modular, and adjustable.
In this blog article, we will go over the SOLID concept in detail, focusing on each of its five components. We will describe each principle, review its advantages, and provide Java examples to demonstrate its application.
Single Responsibility Principle (SRP)
In accordance to the Single Responsibility Principle (SRP), a class should alter for only one cause. In other words, a class should only be responsible for one thing. This makes the code more flexible and manageable.
Take the following class as an illustration of one that breaks the SRP:
public class Employee {
public void calculatePay() {
// Calculate employee's pay
}
public void saveEmployee(Employee employee) {
// Save employee's data to the database
}
}
The Employee
class in the code above is responsible for two tasks: figuring out an employee’s salary and storing the employee’s data in the database. As the class should only have one duty, this goes against the SRP.
We can build two different classes to address this violation:
public class Employee {
public void calculatePay() {
// Calculate employee's pay
}
}
public class EmployeeRepository {
public void saveEmployee(Employee employee) {
// Save employee's data to the database
}
}
The code above splits the responsibilities of calculating an employee’s pay and saving an employee’s data to the database into two distinct classes.
Open/Closed Principle (OCP)
Following to the Open/Closed Principle (OCP), classes should be available for additions but closed for changes. The functionality of a class should therefore be extendable without requiring you to change its source code.
Let’s look at an instance of a class that violates the OCP:
public class DiscountCalculator {
public double calculateDiscount(double price, String discountType) {
double discount = 0;
if (discountType.equals("Christmas")) {
discount = 0.2;
} else if (discountType.equals("Easter")) {
discount = 0.1;
}
return price * (1 - discount);
}
}
The DiscountCalculator
class in the code above has a conditional expression that verifies the discount category. It would be against the OCP to change the source code of the DiscountCalculator
class in order to introduce a new discount category.
We can use strategy pattern to correct this violation:
public interface DiscountStrategy {
double applyDiscount(double price);
}
class ChristmasDiscountStrategy implements DiscountStrategy {
public double applyDiscount(double price) {
return price * 0.8;
}
}
class EasterDiscountStrategy implements DiscountStrategy {
public double applyDiscount(double price) {
return price * 0.9;
}
}
class DiscountCalculator {
private DiscountStrategy discountStrategy;
public DiscountCalculator(DiscountStrategy discountStrategy) {
this.discountStrategy = discountStrategy;
}
public double calculateDiscount(double price) {
return discountStrategy.applyDiscount(price);
}
}
ChristmasDiscountStrategy
and EasterDiscountStrategy
are two classes that implement the DiscountStrategy
interface that was specified in the earlier code. The DiscountCalculator
class has also been altered to support the DiscountStrategy
protocol. Now, without altering the Discount
source code, we can introduce a new discount group by developing a new class that makes use of the DiscountStrategy
interface.
Liskov Substitution Principle (LSP)
The Liskov Substitution Principle (LSP) is a principle in object-oriented programming that states that if a program is using a base class, it should be able to use any of its derived classes without issues. This means that a derived class should not alter the behavior of the base class.
The Liskov replacement concept in Java is demonstrated in the following way:
Let’s say that the class Shape
has a function named getArea()
that identifies the shape’s area. Additionally, we have two derived classes that implement their own getArea()
methods: Circle
and Square
, which are extended from the Shape
class.
If we have a Shape
object, we ought to be able to use getArea()
without any issues on any associated classes (Circle
, Square
, etc.). This ensures that the behavior of the primary class is unaffected by the extended classes.
Here’s an example implementation that follows the Liskov Substitution Principle in Java:
class Shape {
double getArea() {
return 0;
}
}
class Circle extends Shape {
double radius;
Circle(double radius) {
this.radius = radius;
}
double getArea() {
return Math.PI * radius * radius;
}
}
class Square extends Shape {
double side;
Square(double side) {
this.side = side;
}
double getArea() {
return side * side;
}
}
class AreaCalculator {
double calculateArea(Shape shape) {
return shape.getArea();
}
}
In this implementation, the Shape
class has a getArea()
function that gives the shape’s area. We also have two derived classes, Circle
and Square
, which come from Shape
and implement their own implementations of getArea()
.
We have an AreaCalculator
class that takes a Shape
object as an argument to its calculateArea()
method. The method calls the getArea()
method on the Shape
object to calculate its area.
With this implementation, we can call the calculateArea()
method with a Circle
or Square
object, and the method will return the correct area for each object. This demonstrates the Liskov Substitution Principle, as the derived classes (Circle
and Square
) do not break the behavior of the base class (Shape
).
Interface Segregation Principle (ISP)
The Interface Segregation Principle (ISP) is a principle in object-oriented programming that states that a client should not be forced to implement interfaces it does not use.
The principle is designed to help keep interfaces focused and small, and to avoid the unnecessary coupling of classes.
Here’s an illustration of an ISP in Java:
Assume we have a Printer
interface with two methods: printDocument()
and scanDocument()
. We also have two classes that implement the Printer
interface: SimplePrinter
and SmartPrinter
.
The SimplePrinter
class only needs to implement the printDocument()
method, but since it’s implementing the Printer
interface, it’s forced to implement the scanDocument()
method as well, even though it doesn’t use it. This violates the ISP.
Here’s an example implementation that follows the ISP in Java:
interface Printer {
void printDocument();
}
interface Scanner {
void scanDocument();
}
class SimplePrinter implements Printer {
public void printDocument() {
// implementation
}
}
class SmartPrinter implements Printer, Scanner {
public void printDocument() {
// implementation
}
public void scanDocument() {
// implementation
}
}
In this implementation, we have two separate interfaces, Printer
and Scanner
, each with their own methods. We also have two classes, SimplePrinter
and SmartPrinter
, that implement the appropriate interface(s).
The SimplePrinter
class only implements the Printer
interface, and the SmartPrinter
class implements both the Printer
and Scanner
interfaces. This follows the ISP, as each class is only implementing the interfaces it needs, without being forced to implement methods it doesn’t use.
Dependency Inversion Principle (DIP)
According to the Dependency Inversion Principle (DIP), high-level modules should not rely on low-level modules in object-oriented programming. They both ought to rely on ideas.
The idea is to make code more adaptable and straightforward to manage over time.
Here’s a Java example to illustrate the DIP:
Imagine that the TemperatureSensor
class is a dependency of the WeatherTracker
class, which uses it to calculate the temperature. The getTemperature()
function on the TemperatureSensor
is invoked by the WeatherTracker
class’s getCurrentTemperature()
method.
We would also need to modify the WeatherTracker
class if we wanted to switch the TemperatureSensor
to a different kind of monitor. The DIP is broken by this.
interface Sensor {
double getTemperature();
}
class TemperatureSensor implements Sensor {
public double getTemperature() {
// implementation
}
}
class WeatherTracker {
private Sensor sensor;
public WeatherTracker(Sensor sensor) {
this.sensor = sensor;
}
public double getCurrentTemperature() {
return sensor.getTemperature();
}
}
In this implementation, we have an abstract Sensor
interface that defines a getTemperature()
method. We also have a TemperatureSensor
class that implements the Sensor
interface.
The WeatherTracker
class no longer depends on the concrete TemperatureSensor
class, but instead depends on the Sensor
interface. This allows us to swap out the TemperatureSensor
for a different type of sensor that implements the Sensor
interface without changing the WeatherTracker
class. This follows the DIP, as the high-level WeatherTracker
class no longer depends on the low-level TemperatureSensor
class directly.
Conclusion
The SOLID principle is a set of guidelines that help developers create more maintainable code. By adhering to the five concepts of Single Responsibility, Open/Closed, Liskov Substitution, Interface Segregation, and Dependency Inversion, developers can produce software that is more adaptable, understandable, and simple to alter over time.
When writing code, keep in mind these ideas as they can improve the effectiveness and efficiency of software development. By employing SOLID principles, developers can create code that is less prone to mistakes, easier to handle, and more flexible to changing requirements.
Therefore, if you’re a developer, take some time to familiarize yourself with SOLID principles and begin incorporating them into your own writing procedures. You will be well on your way to creating superior, more stable software if you do this.
Resources
- SOLID: The First 5 Principles of Object Oriented Design
- A Solid Guide to SOLID Principles
- SOLID Design Principles Explained: The Single Responsibility Principle
- The Dependency Inversion Principle in Java
- Interface Segregation Principle (ISP)
- Liskov Substitution Principle (LSP)
- 10 Tips for Writing Clean Code
- Single Responsibility Principle Examples
- SOLID Design Principles Explained: The Open/Closed Principle with Code Examples