SOLID PRINCIPLES 

The SOLID principles are fundamental to the design and maintenance of object-oriented programs. They ensure that software is easy to extend, modify, manage, and understand. This article will discuss each principle with Java examples to illustrate how these concepts can be applied in real-world programming. The SOLID principles are a set of design principles that were created to help developers create maintainable, flexible, and scalable code. These principles were introduced by Robert C. Martin in the year 2000 and have since become an essential part of software development. Let’s take a closer look at each principle and how it can be applied in Java. 

  1. S = Single Responsibility Principle 
  1. O= Open/Closed Principle 
  1. L = Liskov Substitution Principle 
  1. I  = Interface Segregation Principle 
  1. D = Dependency Inversion Principle 

1. Single Responsibility Principle (SRP) 

The first principle of SOLID is the Single Responsibility Principle, which states that a class should have only one responsibility. In other words, a class should have only one reason to change. This means that each class should have a single, well-defined purpose and should not be responsible for multiple, unrelated tasks. 

Bad Implementation: 

The class Employee below contain personal details, business logic to perform a few calculations, and DB logic to save/update. 
Our class is tightly coupled and hard to maintain, which may likely have multiple reasons to change in the future. 

 
 
public class Employee { 
 
    private String fullName
    private String dateOfJoining
    private String annualSalaryPackage
 
   // standard getters and setters methods 
 
   // business logic 
 
    public long calculateEmployeeSalary(Employee emp) {…} 
 
    public long calculateEmployeeLeaves(Employee emp) {…} 
 
    public long calculateTax0nSalary(Employee emp) {…} 
 
   // data persistence logic 
 
    public Employee saveEmployee(Employee emp) {…} 
 
    public Employee updateEmployee(Employee emp) {…} 

 

Good Implementation: 

We can split a single Employee class into multiple class as per their specific responsibility. 
This makes our class loosely coupled and easy to maintain with only a single reason to modify. 

 
public class Employee { 
 
    private String fullName
    private String dateofJoining
    private String annualSalaryPackage
 
    // standard getters and setters methods 

 
public class Employeeservice { 
 
    //… 
    public long calculateEmployeeSalary(Employee emp) {…}; 
    public long calculateEmployeeLeaves(Employee emp) {…}; 
    public long calculateTaxOnSalary(Employee emp) {…}; 

 
public class EmployeeDAO { 
 
    //… 
    public Employee saveEmployee(Employee emp) {…}; 
    public Employee updateEmployee(Employee emp) {…}; 

2. Open-Closed Principle (OCP) 

The Open-Closed Principle states that software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification. In other words, we should be able to extend the behaviour of a class without changing its existing code. This helps in keeping our codebase stable and minimizes the risk of introducing new bugs. 

Bad Implementation: 

The class EmployeeSalary below calculates salaries based on employee type: Permanent and Contractual. 

Issue: In the future, if a new employee type (e.g. part-time Employee) comes, then the code needs to be modified to calculate the salary based on this new employee type. 

public class EmployeeSalary { 
 
    public Long calculateSalary(Employee emp) { 
        Long salary = null
 
        if (emp.getType().equals(“PERMANENT”)) { 
 
            salary = (totalWorkingDay * basicPay) + getCompanyBenefits() + getBonus(); 
        } else if (emp.getType().equals(“CONTRACT”)) { 
 
            salary = (totalWorkingDay * basicPay); 
        } 
        return salary; 
    } 

Good Implementation: 

We can introduce a new interface EmployeeSalary and create two child classes for Permanent and Contractual Employees. 

By doing this, when a new type comes then a new child class needs to be created and our core logic will also not change from this. 

public interface EmployeeSalary { 
    public Long calculateSalary(); 

 
public class PermanentEmployeeSalary implements EmployeeSalary { 
    @Override 
    public Long calculateSalary() { 
        return (totalworkingDay * basicPay); 
    } 

 
public class ContractEmployeeSalary implements EmployeeSalary { 
    @Override 
    public Long calculateSalary() { 
        return(totalworkingDay * basicPay) + getCompanyBenefits() + getBonus(); 
    } 

3. Liskov Substitution Principle (LSP) 

The Liskov Substitution Principle states that objects of a superclass should be replaceable with objects of its subclasses without altering the correctness of the program. This principle ensures that the behavior of a subclass should not contradict the behavior of its parent class. 

Bad Implementation: 

The class TeslaToyCar below extends the class Car but does not support the fuel() method as it’s just a toy. That’s why it’s violating the LS principle. 

In our code whereever we’ve used Car, we can’t substitute it directly with TeslaToyCar because fuel() will throw Exception. 

public class Car { 
 
    public void fuel() {…} 
 
    public void wheels() {…} 
 
    public void run() {…} 

 
public class TeslaToyCar extends Car { 
    // Tesla ToyCar inherits Car but does’nt support fuel 
    @Override 
    public void fuel() { 
        throw new IllegalStateException(“Not Supported”); 
    } 
 
    @Override 
    public void run() {…} 
 
    @Override 
    public void wheels() {…} 

 
public class TeslaRealCar extends Car { 
    // Tesla RealCar inherits Car and supports all methods 
    @Override 
    public void fuel() {…} 
 
    @Override 
    public void run() {…} 
 
    @Override 
    public void wheels() {…} 

Good Implementation: 

Creating a new subclass RealCar from the parent Car class, so that RealCar can support fuel() and Car can support generic functions support by any type of car. 

As shown below, TeslaToyCar and TeslaRealCar can be substituted with their respective Parent class. 

public class Car { 
    public void wheels() {…} 
 
    public void run() {…} 

 
public class TeslaToyCar extends Car { 
//    TeslaToyCar inherits Car supports run() and wheels() 
 
    @Override 
    public void run() {…} 
 
    @Override 
    public void wheels() {…} 

 
public class RealCar extends Car { 
    //    RealCar extends Car to support fuel() 
    public void fuel() {…} 

 
public class TeslaRealCar extends RealCar { 
    //    TeslaRealCar inherits Realcar supports fuel(), run() and wheels() 
    @Override 
    public void wheels() {…} 
     
    @Override 
    public void fuel() {…} 
 
    @Override 
    public void run() {…} 

 

4. Interface Segregation Principle (ISP) 

The Interface Segregation Principle states that clients should not be forced to implement interfaces that they do not use. In other words, we should create smaller, more specific interfaces instead of having a single, large interface. 

Bad Implementation: 

Vehicle interface contains the fly() method which is not supported by all vehicles i.e. Bus, Car, etc. Hence, they are forced to provide a dummy implementation. 

It violates the Interface Segregation principle as shown below: 

 
public interface Vehicle { 
 
    void accelerate(); 
 
    void applyBrakes(); 
 
    void fly(); 
 

 
public class Bus implements Vehicle { 
    // Bus provides dummy implementation for fly() method as it can’t fly 
    @Override 
    public void accelerate() {…} 
 
    @Override 
    public void applyBrakes() {…} 
 
    @Override 
    public void fly() {…} 

 
public class Aeroplane implements Vehicle { 
//    Aeroplane implements all methods as it supports all operations 
    @Override 
    public void accelerate() {…} 
 
    @Override 
    public void applyBrakes() {…} 
 
    @Override 
    public void fly() {…} 

Good Implementation: 

Pulling out fly() method into new Flyable interface solves the issue. 

Now, Vehicle interface contains methods supported by all Vehicles. 

And, Aeroplane implements both Vehicle and Flyable interface as it can fly too. 

 
public interface Vehicle { 
 
    void accelerate(); 
 
    void applyBrakes(); 
 

 
public interface Flyable { 
    void fly(); 

 
public class Bus implements Vehicle { 
    // Bus implements only as it doesn’t support fly() 
    @Override 
    public void accelerate() {…} 
 
    @Override 
    public void applyBrakes() {…} 

 
public class Aeroplane implements Vehicle, Flyable { 
    //    Aeroplane implements Vehicle and Flyable 
    @Override 
    public void accelerate() {…} 
 
    @Override 
    public void applyBrakes() {…} 
 
    @Override 
    public void fly() {…} 

5. Dependency Inversion Principle (DIP) 

The Dependency Inversion Principle states that high-level modules should not depend on low-level modules. Both should depend on abstractions. This principle promotes loose coupling between modules, making our code more maintainable and testable. 

Bad Implementation: 

We’ve got a Service class, in which we’ve directly referenced concrete class SQLRepository. 

Issue: Our class is now tightly coupled with SQLRepository, in future if we need to start supporting NoSQLRepository then we need to change Service class. 

class SQLRepository { 
 
    public void save() {…} 
 

 
 
class NoSQLRepository { 
 
    public void save() {…} 
 

 
public class Service { 
 
    //Here we’ve hard-coded SQLRepository 
 
    //in-future if we need to support NoSQLRepository 
 
    //then we need to modify our code 
 
    private SQLRepository repository = new SQLRepository(); 
 
    public void save() { 
 
        repository.save(); 
 
    } 

Good Implementation: 

Create a parent interface Repository and SQL and NoSQL Repository implements it. 

Service class refers to Repository interface, in future if we need to support NoSQL then simply need to pass its instance in constructor without changing Service class. 

interface Repository { 
 
    void save(); 

 
class SQLRepository implements Repository { 
 
    @Override 
    public void save() {…} 
 

 
class NoSQLRepository implements Repository { 
 
    @Override 
    public void save() {…} 
 

 
public class Service { 
 
    private Repository repository
 
    //Here we’re using interface as reference 
 
    //not the concrete class so our code 
 
    //can easily support other child classes 
 
    //of the same interface. 
 
    //For eg: NoSQLRepository class 
 
    public Service(Repository repository) { 
        this.repository = repository; 
    } 
 
    public void save() { 
 
        repository.save(); 
 
    } 

Adopting SOLID principles in Java development not only improves the design and architecture of applications but also aligns well with Java’s object-oriented nature. By adhering to SRP (Single Responsibility Principle), developers create modular classes that are easier to manage. With OCP (Open-Closed Principle), they write code that is more robust against changes. LSP (Liskov Substitution Principle) ensures that the inheritance hierarchies in Java are correctly used, avoiding substitution issues. ISP (Interface Segregation Principle) enhances interface design by keeping them lean and focused. Lastly, DIP (Dependency Inversion Principle) decouples the high-level policies from the low-level details, making the system easier to extend and refactor. 

Integrating SOLID principles into Java programming practices requires a thoughtful approach to design and a commitment to refactoring. However, the long-term benefits—increased code quality, reduced technical debt, and improved software maintainability—are well worth the effort. With a solid understanding of these principles, Java developers can construct systems that stand the test of time and evolve gracefully as new requirements emerge. 

Author
Latest Blogs

SEND US YOUR RESUME

Apply Now