The SOLID principles of Object Oriented Programming are a series of design principles invented by the legendardy Robert Martin also known as Uncle Bob.They are a series of principles to be adhered to in order to create code that is easy to maintain and test.These principles are:
1.Single Responsibility Principle
2.Open Closed Principle
3.Liskov Substituability Principle
4.Interface Segregation Principle
5.Dependency Inversion Principle

1.Single Responsibility Principle
This states that a class should have only one reason to change. This is done by giving the class a single responsibility and hence the name. The reasoning behind this principle is to create software that is simple to understand, easy to maintain and modify and less error prone as it will be dealing with one responsibility. Code that violates this principle will be hard to maintain, understand and modify. An example of code that violates this principle is illustrated below.

Public Class Invoice{
  private int id;
  private String text;
  public Invoice (int id,String text){
   this.id=id;
   this.text=text; 
  }
  public int getId() {return id;}
  public String getText() {return text;}
  public void setID(int id) {this.id=id;}
  public void setText(String text){this.text=text;}
  public void printInvoice(){//code here}
 }

This code has 2 responsibilities.One keeps information about the invoice,the other prints it.If we need to make changes,this could complicate maintainance.To rectify this,we create two classes:one for the invoices,the other for the invoice printing.

Public Class Invoice{
  private int id;
  private String text;
  public Invoice (int id,String text){
   this.id=id;
   this.text=text; 
  }
  public int getId() {return id;}
  public String getText() {return text;}
  public void setID(int id) {this.id=id;}
  public void setText(String text){this.text=text;} 
 }

 Public Class InvoicePrinter{
  private Invoice invoice;
  public InvoicePrinter(Invoice invoice){this.Invoice=invoice;}
  public void printInvoice(){//code here}
 }

The above code separates responsiblities and is easier to maintain.

2.Open/Closed Principle
This states that software should be open to extension but closed to modification. In other words, rather than change the source code of existing classes, we should extend them. This ensures that we dont break any of the existing functionality if we change existing classes and leads to code which is simpler to maintain, extend and use.An example of code that violates this principle is illustrated below.

class PaymentProcessor {
    public void process(String method) {
        if (method.equals("credit")) {
            // process credit card
        } else if (method.equals("paypal")) {
            // process PayPal
        } else if (method.equals("crypto")) {
            // process crypto
        }
    }
}

Every time a new payment method is added, we modify this class. That breaks OCP. Instead,we should use this.

interface PaymentMethod {
    void processPayment();
}

class CreditCardPayment implements PaymentMethod {
    public void processPayment() {
        System.out.println("Processing credit card...");
    }
}

class PayPalPayment implements PaymentMethod {
    public void processPayment() {
        System.out.println("Processing PayPal...");
    }
}

class CryptoPayment implements PaymentMethod {
    public void processPayment() {
        System.out.println("Processing crypto...");
    }
}

class PaymentProcessor {
    public void process(PaymentMethod payment) {
        payment.processPayment();
    }
}

Now we can add new payment types without touching the PaymentProcessor class. You just create a new class implementing PaymentMethod.This allows flexibility and is easy to maintain.

3.Liskov Substitution Principle
This states that any instance of a derived class should be substitutable for an instance of its base class without affecting the correctness of the program. In other words, a derived class should behave like its base class in all contexts or we will break existing functionality. Following this principle can lead to code that is easy to maintain. Violating it will lead to a break in existing functionality and unexpected bugs. An example of code that violates this principle is illustrated below.

interface Bike {
    void turnOnEngine();

    void accelerate();
}
class Motorbike implements Bike {

    boolean isEngineOn;
    int speed;

    @Override
    public void turnOnEngine() {
        isEngineOn = true;
    }

    @Override
    public void accelerate() {
        speed += 5;
    }
}
class Bicycle implements Bike {

    boolean isEngineOn;
    int speed;

    @Override
    public void turnOnEngine() {
        throw new AssertionError("There is no engine!");
    }

    @Override
    public void accelerate() {
        speed += 5;
    }
}

In the above example, we use a Bike Interface and 2 classes which implement it. The second class, Bicycle doesn't have an engine and code which references the Bike Interface will throw errors when the turnOnEngine() method is called. If however, we were to use the following method below, no errors will be thrown.

@Override
    public void turnOnEngine() {
     isEngineOn=true;//substitute human strength for engine
    }

4.Interface Segregation Principle
This states that a client should not be forced to depend on methods that it doesn't use. This principle implies that instead of creating a large interface that covers all the possible methods, it's better to create smaller, more focused interfaces for specific use cases. This approach results in interfaces that are more cohesive and less coupled. An example of code that violates this principle is illustrated below.

interface Worker {
    void work();
    void eat();   // not every worker needs this
}

class Robot implements Worker {
    public void work() {
        System.out.println("Robot working...");
    }

    public void eat() {
        // 🤨 Robots don't eat!
        throw new UnsupportedOperationException("I don't eat!");
    }
}

The above snippet will lead to code that throws errors as the robot class doesn't use the eat method. It will be better to break the code into 2 interfaces as below.

interface Workable {
    void work();
}

interface Eatable {
    void eat();
}

class Human implements Workable, Eatable {
    public void work() {
        System.out.println("Human working...");
    }

    public void eat() {
        System.out.println("Human eating...");
    }
}

class Robot implements Workable {
    public void work() {
        System.out.println("Robot working...");
    }
}

4.Dependency Inversion Principle
This states that High-level modules should not depend on low-level modules but both should depend on abstractions. Abstractions should not depend on details-details should depend on abstractions. This principle aims to reduce coupling between modules, increase modularity, and make the code easier to maintain, test, and extend. For example,

class MySQLDatabase {
    public void save(String data) {
        System.out.println("Saving to MySQL: " + data);
    }
}

class UserService {
    private MySQLDatabase db = new MySQLDatabase(); // 👎 tightly coupled

    public void registerUser(String data) {
        db.save(data);
    }
}

In the above snippet, the principle is violated and we have a situation where UserService is locked to MySQLDatabase and it is hard to swap in-memory DB, Mongo, or mock DB for tests.To rectify this, we use the following.

interface Database {
    void save(String data);
}

class MySQLDatabase implements Database {
    public void save(String data) {
        System.out.println("Saving to MySQL: " + data);
    }
}

class UserService {
    private final Database db;

    public UserService(Database db) { // depends on abstraction
        this.db = db;
    }

    public void registerUser(String data) {
        db.save(data);
    }
}

Special thanks to the following article https://www.freecodecamp.org/news/solid-principles-for-better-software-design/. I borrowed the Bike Interface for the LSP principle. Feel free to comment or share. Have you seen these principles in action in your codebase? Let me know in the comments!