S.O.L.I.D Principles

S.O.L.I.D Principles

The SOLID principles are a set of five design principles in object-oriented programming (OOP) intended to make software designs more understandable, flexible, and maintainable. They were introduced by Robert C. Martin. Each letter in "SOLID" represents one of these principles -

Single Responsibility Principle (SRP)

A class should have only one reason to change, meaning that it should have only one job or responsibility. This principle advocates for keeping classes focused and ensuring that each class is responsible for a single part of the functionality within the system.

Real-world example -

Article content
Medical Instruments

Example of a class violating Single Responsibility Principle

class User {
    public void createUser() {
        //create a user
    }
    
    public void sendEmail() {
        //send an email
    }
}        

In the above example, the User class has two responsibilities: creating a user and sending an email. This violates the SRP. Instead, we should separate these responsibilities into two different classes.

Example of a class following Single Responsibility Principle

class UserCreator {
    public void createUser() {
        // create a user
    }
}

class EmailSender {
    public void sendEmail() {
        // send an email
    }
}        

Open Closed Principle (OCP)

Software entities (classes, modules, functions, etc.) should be open for extension but closed for modification. This means that you should be able to extend the behavior of a system without modifying its existing code. This is often achieved through techniques such as inheritance, composition, and design patterns like the Strategy pattern.

Example of a class violating Open Closed Principle

class Shape {
    public double calculateArea() {
        return 0;
    }
}

class Rectangle extends Shape {
    private double width;
    private double height;

    @Override
    public double calculateArea() {
        return width * height;
    }
}        

In the above example, if we want to add a new shape like a circle, we have to modify the Shape class. This violates the OCP. Instead, we should design our classes so that they can be extended without modification.

Real-world example -

Article content
Electric Adapter

Example of a class following Open Closed Principle

interface Shape {
    double calculateArea();
}

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

    @Override
    public double calculateArea() {
        return width * height;
    }
}

class Circle implements Shape {
    private double radius;

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

Liskov Substitution Principle (LSP)

Objects of a superclass should be replaceable with objects of its subclass without affecting the correctness of the program. In other words, if S is a subtype of T, then objects of type T may be replaced with objects of type S without altering any of the desirable properties of the program.

Real-world example-

Article content
Vehicles

Example of a class violating Liskov Substitution Principle

class Bird {
    public void fly() {
        // Add code for flying
    }
}

class Ostrich extends Bird {
    @Override
    public void fly() {
        throw new UnsupportedOperationException("Ostriches cannot fly");
    }
}        

In the above example, although Ostrich is a subtype of Bird, it does not behave like other birds. This violates the LSP. Instead, we should design our classes so that derived classes can be substituted for their base classes without altering the correctness of the program.

Example of a class following Liskov Substitution Principle

interface Bird {
    void fly();
}

class Sparrow implements Bird {
    @Override
    public void fly() {
        // Add code for flying
    }
}

class Ostrich implements Bird {
    @Override
    public void fly() {
        // Ostriches don't fly
    }
}        

Interface Segregation Principle (ISP)

Clients should not be forced to depend on interfaces they do not use. This principle encourages the design of smaller, more focused interfaces rather than large, monolithic ones. It helps to prevent classes from depending on methods they don't need.

Real-world example-

Article content
Connectors

Example of a class violating Interface Segregation Principle

interface Worker {
    void work();
    void eat();
}

class Robot implements Worker {
    @Override
    public void work() {
        // Add code for working
    }

    @Override
    public void eat() {
        // nothing to do here for a robot
    }
}        

In the above example, the Robot class is forced to implement the eat() method even though it doesn't need it. This violates the ISP. Instead, we should design smaller, more focused interfaces:

Example of a class following Interface Segregation Principle

interface Workable {
    void work();
}

interface Eatable {
    void eat();
}

class Robot implements Workable {
    @Override
    public void work() {
        // Add code for working
    }
}        

Dependency Inversion Principle (DIP)

High-level modules should not depend on low-level modules. Both should depend on abstractions. Abstractions should not depend on details. Details should depend on abstractions. This principle encourages decoupling between classes by introducing abstractions and relying on these abstractions rather than concrete implementations.

Real-world example -

Article content
Wind and Solar Power Station

Example of a class violating Dependency Inversion Principle

class Database {
    public void connect() {
        // Add code to connect to the database
    }
}

class UserService {
    private Database database;

    public UserService() {
        this.database = new Database();
    }
}        

In the above example, UserService directly depends on the Database class, violating the DIP. Instead, we should depend on abstractions rather than concrete implementations.

Example of a class following Dependency Inversion Principle

interface Connection {
    void connect();
}

class Database implements Connection {
    @Override
    public void connect() {
        // Add code to connect to the database
    }
}

class UserService {
    private Connection connection;

    public UserService(Connection connection) {
        this.connection = connection;
    }
}        

In the above example, UserService now depends on the Connection interface rather than the Database class directly. This allows for easier substitution of different types of databases or connection mechanisms.

By adhering to these principles, developers can create more modular, flexible, and maintainable codebases, making it easier to extend and refactor software as requirements change.

To view or add a comment, sign in

More articles by Atul Pawar

Others also viewed

Explore content categories