Codelab Technologies

Introduction

SOLID principles are fundamental principles in object-oriented design that help developers create maintainable, scalable, and flexible software. In this blog post, we’ll explore each SOLID principle with Java code examples to illustrate their importance and practical application.

Section 1: Single Responsibility Principle (SRP)

The Single Responsibility Principle states that a class should have only one reason to change. Let’s consider a class Employee that handles both employee data and persistence.

public class Employee {

 public void saveEmployee(Employee employee) {

 // Save employee to database

 }

 

 public void calculateSalary(Employee employee) {

 // Calculate employee’s salary

 }

 

}

 

This violates SRP because the Employee class has two responsibilities: data management and persistence. We can refactor it to separate concerns:

 

public class Employee {

 // Employee data and methods related to employees

}

 

public class EmployeeRepository {

 public void saveEmployee(Employee employee) {

 // Save employee to database

 }

}

Section 2: Open/Closed Principle (OCP)

The Open/Closed Principle states that software entities should be open for extension but closed for modification. Let’s consider a Shape class hierarchy

public abstract class Shape {

 abstract double area();

}

 

public class Rectangle extends Shape {

 private double width;

 private double height;

 

 // Constructor, getters, and setters

 

 @Override

 double area() {

 return width * height;

 }

}

 

public class Circle extends Shape {

 private double radius;

 

 // Constructor, getters, and setters

 

 @Override

 double area() {

 return Math.PI * radius * radius;

 }

 

}

To add a new shape, such as a Triangle, we would need to modify the Shape class, violating OCP. Instead, we can make Shape closed for modification and open for extension using interfaces:

public interface Shape {

 double area();

}

 

public class Rectangle implements Shape {

 // Rectangle implementation

}

 

public class Circle implements Shape {

 // Circle implementation

}

 

public class Triangle implements Shape {

 // Triangle implementation

 

}

Section 3: Liskov Substitution Principle (LSP)

The Liskov Substitution Principle states that objects of a superclass should be replaceable with objects of a subclass without affecting the correctness of the program. Let's consider a scenario with Bird and Duck.

public class Bird {

 public void fly() {

 // Bird flying behavior

 }

}


public class Duck extends Bird {

 public void quack() {

 // Duck quacking behavior

 }

}

If we have code that expects a Bird but doesn’t handle the case of a Duck properly, LSP is violated. We can refactor to ensure substitutability:

public interface Bird {

 void fly();

}


public class Sparrow implements Bird {

 // Sparrow implementation

}


public class Duck implements Bird {

 // Duck implementation

}

Section 4: Interface Segregation Principle (ISP)

The Interface Segregation Principle states that clients should not be forced to depend on interfaces they do not use. Let’s consider an interface Worker that includes methods for both working and eating.

public interface Worker {

 void work();

 void eat();

}

 

public class Engineer implements Worker {

 // Engineer implementation

}

 

public class Chef implements Worker {

 // Chef implementation

 

}

If a client only needs to interact with workers to assign tasks, it’s forced to depend on the eat() method, violating ISP. We can refactor by splitting the interface:

public interface Worker {

 void work();

}

 

public interface Eater {

 void eat();

 

}

public class Engineer implements Worker {

 // Engineer implementation

}

 

public class Chef implements Worker, Eater {

 // Chef implementation

 

}

 

Section 5: Dependency Inversion Principle (DIP)

The Dependency Inversion Principle states that high-level modules should not depend on low-level modules but rather on abstractions. Let’s consider a Notification Service that depends on a Email Sender.

public class NotificationService {

 private EmailSender emailSender;

 

 public NotificationService() {

 this.emailSender = new EmailSender();

 }

 

 public void sendNotification() {

 emailSender.sendEmail();

 }

}

 

public class EmailSender {

 public void sendEmail() {

 // Send email

 }

 

}

This tightly couples Notification Service to Email Sender, violating DIP. We can refactor to depend on an abstraction:

public interface MessageSender {

 void sendMessage();

}

 

public class EmailSender implements MessageSender {

 @Override

 public void sendMessage() {

 // Send email

 }

}

 

public class NotificationService {

 private MessageSender messageSender;

 

 public NotificationService(MessageSender messageSender) {

 this.messageSender = messageSender;

 }

 

 public void sendNotification() {

 messageSender.sendMessage();

 }

 

}

Conclusion

In this post, we’ve explored the SOLID principles in Java with practical code examples. By adhering to these principles, developers can create software that is easier to maintain, extend, and understand. Incorporating SOLID principles into your Java projects can lead to cleaner, more robust codebases.