SOLID Design Principles in Software Development

Nivedita KumariNivedita Kumari
7 min read

The SOLID design principles are a set of five principles that help developers create software that is easy to use, maintain, and extend.

  1. S- Single Responsibility Principle(SRP)

  2. O- Open/Closed Principle(OCP)

  3. L-Liskov Substitution Principle(LSP)

  4. I-Interface Segregation Principle(ISP)

  5. D-Dependency Ineversion Principle(DIP)

Single Responsibility Principle (SRP)

There should be only one reason to change any class , module and function.It means every class, module and function should have only one responsibility.

Example:

Without SRP

package SOLID.SRP.WOT;

class Employee {
    private String name;
    private double hourlyRate;
    private int hoursWorked;

    public Employee(String name, double hourlyRate, int hoursWorked) {
        this.name = name;
        this.hourlyRate = hourlyRate;
        this.hoursWorked = hoursWorked;
    }

    public double calculateSalary() {
        return hourlyRate * hoursWorked;
    }

    public void saveToDatabase() {
        System.out.println("Saving " + name + " to database...");
    }
}

Here, Employee class has not only employee data but it is also calculating salary and saving data to database.

Employee class has multiple responsibility that’s why violates SRP.

With SRP

package SOLID.SRP.WT;

public class Employee {

    private String name;
    private double hourlyRate;
    private int totalHours;

    Employee(String name, double hourlyRate) {
        this.name = name;
        this.hourlyRate = hourlyRate;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public void setHourlyRate(double hourlyRate) {
        this.hourlyRate = hourlyRate;
    }

    public double getHourlyRate() {
        return hourlyRate;
    }

    public void setTotalHours(int totalHours) {
        this.totalHours = totalHours;
    }

    public int getTotalHours() {
        return totalHours;
    }
}
package SOLID.SRP.WT;

public class SalaryCalculation {
    public double calculateSalary(Employee employee) {
        return employee.getHourlyRate()*employee.getTotalHours();
    }
}
package SOLID.SRP.WT;

public class SaveEmployeeToDB {
    public void saveToDatabase(Employee employee) {
        System.out.println("Saving " + employee.getName() + " to database...");
    }
}

here Every class has only one responsibility , these classes supporting SRP.

Open/Closed Principle(OCP)

A class should be open for extension but closed for modification.

Simple Meaning:

  • You should be able to add new features without changing existing code.

  • Instead of modifying a class, you should extend it using inheritance or polymorphism.

Violates OCP

package SOLID.OCP.ViolateOCP;

class PaymentProcessor {
    public void processPayment(String paymentType, double amount) {
        if (paymentType.equals("CreditCard")) {
            System.out.println("Processing credit card payment of $" + amount);
        } else if (paymentType.equals("PayPal")) {
            System.out.println("Processing PayPal payment of $" + amount);
        }
    }
}

If we want to add any other payment method then we have to modify this PaymentProcessor class.

Follows OCP

package SOLID.OCP.FollowOCP;

public interface PaymentMethod {
    public  void payment(double amount);
}
package SOLID.OCP.FollowOCP;

public class CreditCardPayment implements PaymentMethod{

    @Override
    public void payment(double amount) {
        System.out.println("Processing credit card payment of Rs." + amount);
    }
}
package SOLID.OCP.FollowOCP;

public class DebitCardPayment implements PaymentMethod{
    @Override
    public void payment(double amount) {
        System.out.println("Processing debit card payment of Rs." + amount);
    }
}
package SOLID.OCP.FollowOCP;

public class PaymentProcessor {

    public void processPayment(PaymentMethod paymentMethod, double amount) {
        paymentMethod.payment(amount);
    }
}
package SOLID.OCP.FollowOCP;

public class Main {
    public static void main(String[] args) {
        DebitCardPayment debitCardPayment = new DebitCardPayment();
        CreditCardPayment creditCardPayment = new CreditCardPayment();
        PaymentProcessor processor = new PaymentProcessor();

        processor.processPayment(debitCardPayment,200.0);
    }
}

Here we have two types of payment method debit and credit .

suppose we want to add a new payment method that is Upi payment method.

we just need to create a new class that will implement PaymentMethod interface just like DebitCardPayment and CreditCardPayment

package SOLID.OCP.FollowOCP;

public class UPIPayment implements PaymentMethod{
    @Override
    public void payment(double amount) {
        System.out.println("Processing upi payment of Rs." + amount);
    }
}

Here We didn’t modify any class just extended PaymentMethod class.

Liskov Substitution Principle(LSP)

A child class must be able to replace its parent class without breaking the functionality of the program.

Key Points:

A subclass should be able to replace a superclass without issues.
If a child class doesn’t fully behave like its parent, refactor the hierarchy!
Use interfaces and separate behaviors when needed.

Violates LSP

package SOLID.LSP.Violate;

public class Vehicle {
    public void startEngine() {
        System.out.println("Engine has started!");
    }
}
package SOLID.LSP.Violate;

public class Car extends Vehicle{
}
package SOLID.LSP.Violate;

public class Cycle extends Vehicle{
}
package SOLID.LSP.Violate;

public class Main {
    public static void main(String[] args) {
        Car car = new Car();
        car.startEngine();

        Cycle cycle = new Cycle();
        cycle.startEngine(); // Cycle does NOT have an engine, but forced to implement startEngine()
    }
}

Follows LSP

package SOLID.LSP.Follow;

public class Vehicle {
    public  void move() {
        System.out.println("Vehicle is moving....");
    }
}
package SOLID.LSP.Follow;

public interface EnginePowered {
    public void startEnginee();
}
package SOLID.LSP.Follow;

import SOLID.LSP.Follow.Vehicle;

public class Car extends Vehicle implements EnginePowered {

    @Override
    public void startEnginee() {
        System.out.println("Engine has started....");
    }
}
package SOLID.LSP.Follow;

import SOLID.LSP.Follow.Vehicle;

public class Cycle extends Vehicle {

}
package SOLID.LSP.Follow;

import SOLID.LSP.Follow.Car;
import SOLID.LSP.Follow.Cycle;
import SOLID.LSP.Follow.Vehicle;

public class Main {
    public static void main(String[] args) {
        Car car = new Car();
        Cycle cycle = new Cycle();

        car.move();
        car.startEnginee();
        cycle.move(); //cycle can't call startEngine which is correct



    }
}

Interface Segregation Principle(ISP)

What is ISP?

A class should not be forced to implement interfaces it does not use.

Instead of one large interface, break it into smaller, specific interfaces.
A class should only depend on methods that are relevant to it.

Use multiple interfaces for different behaviors.

Violates ISP

package SOLID.ISP.Violates;

public interface Printer {
    public  void print();
    public void scan();
    public void fax();
}
package SOLID.ISP.Violates;

public class BasicPrinter implements  Printer{

    @Override
    public void print() {
        System.out.println("Basic Printing.....");

    }

    @Override
    public void scan() {
        System.out.println("Basic Scanning....."); // doesn't support still have to implement
    }

    @Override
    public void fax() {
        System.out.println("Basic Faxing....."); // doesn't support still have to implement
    }
}
package SOLID.ISP.Violates;

public class AdvancePrinter implements  Printer{
    @Override
    public void print() {
        System.out.println("Advanced Printing.....");

    }

    @Override
    public void scan() {
        System.out.println("Advanced Scanning.....");
    }

    @Override
    public void fax() {
        System.out.println("Advanced Faxing.....");
    }
}
package SOLID.ISP.Violates;

public class Main {
    public static void main(String[] args) {
        BasicPrinter basicPrinter = new BasicPrinter();
        AdvancePrinter advancePrinter = new AdvancePrinter();

        basicPrinter.print();
        basicPrinter.scan(); // but this printer doesn't support this functionality
        basicPrinter.fax(); // but this printer doesn't support this functionality
    }
}

Here basic printer doesn’t support scan and fax still , still it has to implement these functions which violates ISP.

Follows ISP

package SOLID.ISP.Follows;

public interface Printable {
    public  void print();
}
package SOLID.ISP.Follows;

public interface Scanable {
    public  void scan();
}
package SOLID.ISP.Follows;

public interface Faxable {
    public  void fax();
}
package SOLID.ISP.Follows;

public class BasicPrinter implements  Printable{
    @Override
    public void print() {
        System.out.println("Basic Printing....");
    }
}
package SOLID.ISP.Follows;

import SOLID.ISP.Violates.Printer;

public class AdvancePrinter implements Printable,Scanable,Faxable {

    @Override
    public void print() {
        System.out.println("Advance Printing....");
    }

    @Override
    public void scan() {
        System.out.println("Advance Scanning....");
    }

    @Override
    public void fax() {
        System.out.println("Advance faxing.....");
    }
}
package SOLID.ISP.Follows;



public class Main {
    public static void main(String[] args) {
        BasicPrinter basicPrinter = new BasicPrinter();
        AdvancePrinter advancePrinter = new AdvancePrinter();
        basicPrinter.print();
        advancePrinter.fax();
        advancePrinter.print();
        advancePrinter.scan();
    }
}

Here BasicPrinter only supports printing. it has not any other unnecessary functions.

Dependency Ineversion Principle(DIP)

What is DIP?

High-level modules should not depend on low-level modules.
Both should depend on abstractions (interfaces).
Abstractions should not depend on details. Details should depend on abstractions.(This means high-level code (business logic) should depend on interfaces or abstract classes, not on concrete implementations.)

Why is DIP important?

If high-level modules directly depend on low-level modules, code becomes tightly coupled.

Changing one part of the system can break other parts.
DIP makes the code flexible and easy to maintain.

Higher-level and lower-level classes should depend on interfaces or abstract classes instead of concrete implementations.

Violates DIP

package SOLID.DIP.Violates;

// Low-Level-class
public class PaypalPayment {

    public void pay(double amount) {
        System.out.println("Payment is getting Processed RS." + amount);
    }
}
package SOLID.DIP.Violates;

// Order-service is high level module
public class OrderService {
    public PaypalPayment paypalPayment;
    public OrderService() {
        this.paypalPayment= new PaypalPayment();
    }

    public void processOrder(double amount) {
        paypalPayment.pay(amount);
        System.out.println("Order processed!");
    }
}
package SOLID.DIP.Violates;

public class Main {
    public static void main(String[] args) {
        OrderService orderService = new OrderService();
        orderService.processOrder(700);
    }
}

here OrderService is tightly coupled with PayPalPayment.

  • Adding new payment methods (Stripe, Razorpay) requires modifying OrderService.

Follows DIP

package SOLID.DIP.Follows;


public interface PaymentMethod {
    public  void pay(double amount);
}
package SOLID.DIP.Follows;



public class PayPalPayment implements  PaymentMethod {
    @Override
    public void pay(double amount) {
        System.out.println("Payment is getting Processed  through PayPal RS." + amount);
    }
}
package SOLID.DIP.Follows;

public class UPI implements  PaymentMethod{
    @Override
    public void pay(double amount) {
        System.out.println("Payment is getting Processed  through UPI RS." + amount);
    }
}
package SOLID.DIP.Follows;

import SOLID.DIP.Follows.PaymentMethod;

public class OrderService {
    public PaymentMethod paymentProcessor;
    public OrderService( PaymentMethod paymentProcessor) {
        this.paymentProcessor = paymentProcessor;
    }

    public void processOrder(double amount) {
        paymentProcessor.pay(amount);
        System.out.println("Order processed!");
    }
}
package SOLID.DIP.Follows;

import SOLID.DIP.Follows.OrderService;

public class Main {
    public static void main(String[] args) {
        OrderService orderService = new OrderService(new UPI());
        orderService.processOrder(300);
        orderService = new OrderService(new PayPalPayment());
        orderService.processOrder(200);

    }
}

Why This is Better?

1.OrderService does not depend on PayPal or UPI. It depends on the PaymentMethod abstraction.
2.Adding new payment methods does not require modifying OrderService.
3.More flexible, reusable, and maintainable design.

1
Subscribe to my newsletter

Read articles from Nivedita Kumari directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Nivedita Kumari
Nivedita Kumari

I am wokring as SDE-1 at BetterPlace