DEV Community

André Moriya
André Moriya

Posted on

SOLID Principles

Everyone knows, but it's worth remembering 😂

Hi fellow developers!

My topic today is about SOLID. I decided to write about it to learn more and share this content with you.

SOLID is a set of 5 essential principles that help developers write clean, maintainable, and scalable object-oriented code. In this article, we’ll dive deep into each principle with clear explanations and real Java examples.


📌 What is SOLID?

S.O.L.I.D is an acronym introduced by Robert C. Martin (Uncle Bob) that stands for:

  • S: Single Responsibility Principle
  • O: Open/Closed Principle
  • L: Liskov Substitution Principle
  • I: Interface Segregation Principle
  • D: Dependency Inversion Principle

These principles guide developers in designing software that’s:

  • Easier to maintain
  • More reusable
  • More readable
  • Less prone to bugs
  • Easier to test and scale

Let's take a closer look at each letter.


🔹 1. Single Responsibility Principle (SRP)

📌 “A class should have only one reason to change.” Each class should do one thing and do it well.

✅ Why?
If a class has multiple responsibilities, changes to one responsibility might break the others.

✅ Example:
Use interfaces or abstract classes and inheritance to extend behavior.

🛑 Bad Example:

public class InvoiceService {
    public void calculateTotal() { /* ... */ }
    public void saveToDatabase() { /* ... */ }
    public void printInvoice() { /* ... */ }
}
Enter fullscreen mode Exit fullscreen mode

In my opinion this class isn't a good example to try to explain this principle. I don't have any idea in my mind, but I hope it helps you understand.🤣
This class mixes business logic, persistence, and presentation - all in one. 😁 This situation is not something you would encounter in real life, in a real project or on your project. At least that's what I hope 🙏😅
This class is hard to test, maintain and opened to create a bug

✅ Good Example:

public interface Calculator<T> {
    voic calculate(T input);
}
public class InvoiceCalculator implements Calculator<Invoice> {
    public void calculate(Invoice input) { /* ... */ }
}

public interface InvoiceRepository {
    public void save(Invoice invoice);
}

public class InvoiceDAO implements InvoiceRepository {
    public void save(Invoice invoice) { /* ... */}
}

public interface Printer<T> {
    public void print(T input);
}

public class InvoicePrinter implements Printer<Invoice> {
    public void print(Invoice info) {/* ... */}
}
Enter fullscreen mode Exit fullscreen mode

Each class now has a single responsibility.

Maybe, we could use a functional interface like Function or Consumer and take advantage of java language features.
What's do you think about it?


🔹 2. Open/Closed Principle (OCP)

📌 “Software entities should be open for extension but closed for modification.”

✅ Why?
Modifying existing code introduces risk. Extending it (e.g., via inheritance or composition) avoids breaking tested code.

🛑 Bad Example:

public class Payment {
    public void process(String method) {
        if (method.equals("CreditCard")) {
            // ...
        } else if (method.equals("PayPal")) {
            // ...
        }
    }
}

Enter fullscreen mode Exit fullscreen mode

✅ Good Example:

public interface PaymentMethod {
    void pay();
}

public class CreditCardPayment implements PaymentMethod {
    public void pay() { System.out.println("Paying with credit card"); }
}

public class PayPalPayment implements PaymentMethod {
    public void pay() { System.out.println("Paying with PayPal"); }
}

public class PaymentProcessor {
    public void process(PaymentMethod method) {
        method.pay(); // Easily extend by adding new PaymentMethod types
    }
}

Enter fullscreen mode Exit fullscreen mode

No need to modify PaymentProcessor to add new methods — just extend PaymentMethod.

🔹 3. Liskov Substitution Principle (LSP)

📌 “Subtypes must be substitutable for their base types.”
A subclass should be able to replace its parent class without breaking the program.

✅ Why?

Violating this leads to code that crashes or behaves incorrectly when using polymorphism.

🛑 Bad Example:

class Bird {
    public void fly() { System.out.println("Flying"); }
}

class Ostrich extends Bird {
    @Override
    public void fly() { 
        throw new UnsupportedOperationException()
    }
}
Enter fullscreen mode Exit fullscreen mode

Calling fly() on Bird works for most birds, but fails for Ostrich — violating LSP.

✅ Good Example (Refactored Design):

abstract class Bird {}

interface Flyable {
    void fly();
}

class Sparrow extends Bird implements Flyable {
    public void fly() { System.out.println("Sparrow flying"); }
}

class Ostrich extends Bird {
    // Doesn't implement Flyable
}
Enter fullscreen mode Exit fullscreen mode

Now, only birds that can fly implement Flyable, respecting LSP.

🔹 4. Interface Segregation Principle (ISP)

📌 “Clients should not be forced to depend on methods they do not use.”

Split large, bloated interfaces into smaller, more specific ones.

✅ Why?
A class should only implement the methods it needs. Large interfaces cause unnecessary complexity.

🛑 Bad Example:

public interface Machine {
    void print();
    void scan();
    void fax();
}

public class SimplePrinter implements Machine {
    public void print() { System.out.println("Printing..."); }
    public void scan() { /* Not needed */ }
    public void fax() { /* Not needed */ }
}
Enter fullscreen mode Exit fullscreen mode

This forces SimplePrinter to implement unused methods.

✅ Good Example:

public interface Printer {
    void print();
}

public interface Scanner {
    void scan();
}

public class SimplePrinter implements Printer {
    public void print() { System.out.println("Printing..."); }
}
Enter fullscreen mode Exit fullscreen mode

Each class now depends only on what it actually uses.

🔹 5. Dependency Inversion Principle (DIP)

📌 “High-level modules should not depend on low-level modules. Both should depend on abstractions.”

Depend on interfaces, not concrete implementations.

✅ Why?
This promotes flexibility, testability, and decoupling.

🛑 Bad Example:

public class OrderService {
    private PaypalProcessor processor = new PaypalProcessor(); // tightly coupled
    public void checkout() {
        processor.pay();
    }
}
Enter fullscreen mode Exit fullscreen mode

✅ Good Example:

public interface PaymentProcessor {
    void pay();
}

public class PaypalProcessor implements PaymentProcessor {
    public void pay() { System.out.println("Paid with PayPal"); }
}

public class OrderService {
    private final PaymentProcessor processor;

    public OrderService(PaymentProcessor processor) {
        this.processor = processor;
    }

    public void checkout() {
        processor.pay();
    }
}
Enter fullscreen mode Exit fullscreen mode

Now, we can easily swap PaypalProcessor for another processor or mock it in tests.

📗 Summary Table

Principle Focus Goal
S Single Responsibility One reason to change
O Open/Closed Extend without modifying
L Liskov Substitution Replace parent without breaking behavior
I Interface Segregation Don’t force classes to depend on unused methods
D Dependency Inversion Depend on abstractions, not concrete classes

Done!
I hope this post is useful for me 😅 and you now and in the future, and that you will find it helpful. 🙏
If you have any opinions, criticisms or ideas, don't hesitate to get in touch.

Thank you very much
See ya!

Top comments (0)