DEV Community

Sadisha Nimsara
Sadisha Nimsara

Posted on • Originally published at nsadisha.Medium on

SOLID Principles

Cover image for SOLID Principles

SOLID is a collection of fundamental principles designed to enhance the manageability and scalability of code in Object-Oriented Programming (OOP). It consists of five key principles:

  1. S ingle Responsibility Principle — SRP
  2. O pen-Closed Principle — OCP
  3. L iskov’s Substitution Principle — LSP
  4. I nterface Segregation Principle — ISP
  5. D ependency Inversion Principle — DIP

These principles were introduced by Robert C. Martin (also known as Uncle Bob) in the early 2000s and have since become widely adopted in the software development community. By following SOLID principles, developers can create code that is easier to understand, modify, and extend, leading to more robust and maintainable software systems.

Single Responsibility Principle (SRP)

Single Responsibility Principle is the first and most fundamental principle in OOP and SOLID. As the name sounds, this principle means “One class should have only one specific responsibility to take care of”.

Suppose we have a class called Invoice , which contains 2 methods generateInvoice() and saveToFiles() .

public class Invoice {
  private Long InvoiceNo;

  public void generateInvoice() {
    // code to generate Invoice.
  }

  public void saveToFiles() {
    // code to save invoice as a file.
  }
}
Enter fullscreen mode Exit fullscreen mode

This is not a good practice because the Invoice class has two responsibilities. A better approach would be to separate these functionalities into dedicated classes.

public class Invoice {
  private Long InvoiceNo;

  public void generateInvoice() {
    // code to generate Invoice.
  }
}

public class FileManager {
  public void saveToFiles(Invoice invoice) {
    // code to save invoice as a file.
  }
}
Enter fullscreen mode Exit fullscreen mode

Here, we can see we have 2 classes for the use case:

  • Generating the invoice
  • Save it to files

Benefits of following SRP

  • Improved Code Organization : By separating concerns into different classes, the codebase becomes more organized and easier to navigate.
  • Better Maintainability : When a class has a single responsibility, it is easier to understand its purpose and make changes without unintended side effects.
  • Increased Reusability : Classes with a single responsibility are more likely to be reusable in different parts of the application or even in other projects.
  • Easier Testing : Classes with a single responsibility are typically smaller and more focused, making them easier to test in isolation.

Open-Closed Principle (OCP)

Open-Closed principle is another core principle in SOLID. This principle was introduced by Bertrand Meyer in 1997. The idea behind this principle is “Software artifacts (classes, modules, and functions) should open for extensions, but closed for modifications.”

For example;

Let’s say, we have a class called Shape , we can use this class to calculate the area of the shape.

public class Shape {
    private String shapeType;
    private double radius;
    private double length;
    private double width;

    public Shape(String shapeType, double radius, double length, double width) {
        this.shapeType = shapeType;
        this.radius = radius;
        this.length = length;
        this.width = width;
    }

    public double area() {
        if (shapeType.equals("circle")) {
            return Math.PI * (radius * radius);
        } else if (shapeType.equals("rectangle")) {
            return length * width;
        } else {
            throw new IllegalArgumentException("Unknown shape type");
        }
    }
}

// Usage
public class Main {
    public static void main(String[] args) {
        Shape circle = new Shape("circle", 5, 0, 0);
        Shape rectangle = new Shape("rectangle", 0, 4, 6);

        System.out.println(circle.area());
        System.out.println(rectangle.area());
    }
}
Enter fullscreen mode Exit fullscreen mode

In the code above, adding a new shape requires modifying the existing Shape class, which is not considered a good practice.

Below is a code example that demonstrates how to apply the Open-Closed Principle to this scenario.

public interface Shape {
  public double area();
}

class Circle implements Shape {
    private double radius;

    public Circle(double radius) {
        this.radius = radius;
    }

    @Override
    public double area() {
        return Math.PI * (radius * radius);
    }
}

class Rectangle implements Shape {
    private double length;
    private double width;

    public Rectangle(double length, double width) {
        this.length = length;
        this.width = width;
    }

    @Override
    public double area() {
        return length * width;
    }
}

// Usage
public class Main {
    public static void main(String[] args) {
        Shape circle = new Circle(5);
        Shape rectangle = new Rectangle(4, 6);

        System.out.println(circle.area());
        System.out.println(rectangle.area());
    }
}
Enter fullscreen mode Exit fullscreen mode

With the application of OCP, we can add many shapes as we want without modifying the current implementation.

NOTE: Using interfaces is not the only way to achieve OCP.

Benefits of following OCP

  • Reduced Risk of Bugs : By not modifying existing code, the risk of introducing new bugs or breaking existing functionality is minimized.
  • Improved Maintainability : Code that follows the OCP is easier to maintain and extend, as new features can be added without altering the existing codebase.
  • Enhanced Flexibility : The use of abstractions and polymorphism allows for more flexible and adaptable designs, making it easier to accommodate changing requirements.

Liskov’s Substitution Principle (LSP)

Liskov’s Substitution Principle is another important principle in OOP. It was introduced by Barbara Liskov in 1987 during a conference talk on data abstraction.

The principle states, “Objects of a superclass should be replaceable with objects of its subclasses without altering the correctness of the program”.

For example, if Circle and Rectangle are sub types of Shape, then we should be able to replace Shape object with a Circle or Rectangle object without any issues.

public interface Shape {
  public double area();
}

class Circle implements Shape {
    private double radius;

    public Circle(double radius) {
        this.radius = radius;
    }

    @Override
    public double area() {
        return Math.PI * (radius * radius);
    }
}

class Rectangle implements Shape {
    private double length;
    private double width;

    public Rectangle(double length, double width) {
        this.length = length;
        this.width = width;
    }

    @Override
    public double area() {
        return length * width;
    }
}

public class Main {
    public static void main(String[] args) {
        Shape circle = new Circle(5); // Replace Shape object with Circle
        Shape rectangle = new Rectangle(4, 6); // Replace Shape object with Rectangle

        System.out.println(circle.area());
        System.out.println(rectangle.area());
    }
}
Enter fullscreen mode Exit fullscreen mode

As demonstrated in this example, adhering to the Liskov Substitution Principle means we should be able to substitute a superclass instance with a subclass instance seamlessly.

Benefits of following LSP

  • Improved Code Reusability : By ensuring that subtypes can be substituted for their base types, code that uses the base type can also work with any of its subtypes, promoting code reuse.
  • Enhanced Maintainability : Code that follows LSP is easier to maintain because it reduces the risk of introducing bugs when modifying or extending the codebase.
  • Better Testability : LSP makes it easier to write unit tests for classes and their subtypes, as the tests can be written against the base type and should work for all subtypes.

Interface Segregation Principle (ISP)

The Interface Segregation Principle is one of the five SOLID principles introduced by Robert C. Martin. It states: “Clients should not be forced to depend on interfaces they do not use”.

In other words, Using many task specific interfaces is better than using one general purpose interface.

Below example shows the usage of general purpose interface.

public interface MultifunctionPrinter {
    void printDocument(Document document);
    void scanDocument(Document document);
    void faxDocument(Document document);
}

public class BasicPrinter implements MultifunctionPrinter {
    @Override
    public void printDocument(Document document) {
        System.out.println("Printing: " + document.name());
    }

    @Override
    public void scanDocument(Document document) {
        throw new UnsupportedOperationException("Scanning not supported.");
    }

    @Override
    public void faxDocument(Document document) {
        throw new UnsupportedOperationException("Faxing not supported.");
    }
}
Enter fullscreen mode Exit fullscreen mode

Using a general-purpose interface like MultifunctionPrinter forces us to implement unnecessary methods, which is considered bad practice. Let’s explore how we can apply the Interface Segregation Principle to this scenario.

Interfaces

public interface Printer {
    void printDocument(Document document);
}

public interface Scanner {
    void scanDocument(Document document);
}

public interface Fax {
    void faxDocument(Document document);
}
Enter fullscreen mode Exit fullscreen mode

Implementations

public class BasicPrinter implements Printer {
    @Override
    public void printDocument(Document document) {
        System.out.println("Printing: " + document.name());
    }
}

public class AdvancedPrinter implements Printer, Scanner {
    @Override
    public void printDocument(Document document) {
        System.out.println("Printing: " + document.name());
    }

    @Override
    public void scanDocument(Document document) {
        System.out.println("Scanning: " + document.name());
    }
}

public class FaxMachine implements Fax {
    @Override
    public void faxDocument(Document document) {
        System.out.println("Faxing: " + document.name());
    }
}
Enter fullscreen mode Exit fullscreen mode

By applying the ISP , we split it into smaller, role-specific interfaces — like Printer, Scanner, and Fax. This allows each class (e.g. BasicPrinter, AdvancedPrinter, or FaxMachine) to implement only the relevant functionality, promoting modularity and reducing unnecessary dependencies.

Benefits of following ISP

  • Modular and Reusable Code : By breaking down large interfaces into smaller, more specific ones, the code becomes more modular and reusable. Classes or modules can implement only the interfaces they need, reducing unnecessary dependencies and making it easier to reuse code across different parts of the system.
  • Reduced Code Complexity : When classes or modules depend only on the interfaces they need, the code becomes less complex and easier to understand. This is because developers do not have to deal with unnecessary methods or dependencies. These are not relevant to their specific use case.
  • Improved Maintainability : With smaller and more focused interfaces, it becomes easier to maintain the code. Changes to one interface are less likely to affect other parts of the system, reducing the risk of introducing bugs or breaking existing functionality.
  • Better Testability : Smaller and more focused interfaces make it easier to write unit tests for individual components. This is because the tests can focus on specific behaviors without being affected by irrelevant methods or dependencies.
  • Increased Flexibility : By adhering to the ISP, the system becomes more flexible and easier to extend or modify. New features or requirements can be added by creating new interfaces or modifying existing ones without affecting the entire system.

Dependency Inversion Principle (DIP)

Dependency Inversion Principle is the final principle of SOLID. Which was also introduced by Robert C. Martin. This promotes loosely-coupled code.

DIP states few points:

  • High-level modules should not depend on low-level modules.
  • Both should depend on abstraction.
  • Abstraction should not depend on details.
  • Details should depend on abstraction.

In simple terms, instead of a class directly depending on other specific classes (concrete implementations), it should depend on interfaces or abstract classes. This makes the code more flexible and easier to maintain, as you can swap out implementations without changing the dependent class.

Tightly coupled code (without DIP)

public class Keyboard {
    public void type() {
        System.out.println("Typing...");
    }
}

public class Computer {
    private Keyboard keyboard;

    public Computer() {
        this.keyboard = new Keyboard(); // Direct dependency
    }

    public void use() {
        keyboard.type();
    }
}
Enter fullscreen mode Exit fullscreen mode

As shown in the example above, the Computer class directly depends on the Keyboard class.

Loosely coupled code (with DIP)

public interface InputDevice {
    void type();
}

pubic class Keyboard implements InputDevice {
    @Override
    public void type() {
        System.out.println("Typing...");
    }
}

public class Computer {
    private InputDevice inputDevice;

    public Computer(InputDevice inputDevice) {
        this.inputDevice = inputDevice; // Depends on abstraction
    }

    public void use() {
        inputDevice.type();
    }
}
Enter fullscreen mode Exit fullscreen mode

Now, Computer depends on the InputDevice interface, not a specific Keyboard. This makes it easy to switch to another input device, like a WirelessKeyboard, without modifying the Computer class.

Benefits of following DIP

  • Loose Coupling : By depending on abstractions rather than concrete implementations, the code becomes less tightly coupled, making it easier to change one part of the system without affecting others.
  • Improved Maintainability : Changes in low-level modules do not impact high-level modules, making the system easier to maintain and extend.
  • Enhanced Testability : High-level modules can be tested using mock implementations of the low-level modules, making testing faster and more reliable.
  • Increased Reusability : High-level modules can be reused in different contexts without needing to change the low-level modules they depend on.

Conclusion

In conclusion, the SOLID principles: Single Responsibility, Open-Closed, Liskov Substitution, Interface Segregation, and Dependency Inversion provide essential guidelines for writing clean, maintainable, and scalable code in object-oriented programming.

By adhering to these principles, developers can create systems that are easier to understand, modify, and extend, ultimately leading to higher quality software and more efficient development processes.

Summary

Thank you for reading this article! I hope you now have a solid understanding of the SOLID principles and how you can apply them to enhance your projects.

Follow me on:

— Sadisha Nimsara

Top comments (1)

Collapse
 
netch80 profile image
Valentin Nechayev

SOLID is a really silly combination of exactly 5 principles among much wider set (a few dozens) of them, gained popularity only due to the fine acronym and Robert Martinʼs PR.

In turn, Robert Martin is kinda info fraudster who sells soft skills disguised into hard skills. Use of his terms established a false image of good cooperation between programmers and nearest levels of their authority - this is the only reason to select them and the base set of "design patterns".

To be not limited to these 5 principles, it's better to start at least with GRASP (also extremely limited, but a bit better as a starting base) and then continue into wider sets.