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)