This content originally appeared on DEV Community and was authored by André Moriya
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() { /* ... */ }
}
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) {/* ... */}
}
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")) {
// ...
}
}
}
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
}
}
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()
}
}
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
}
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 */ }
}
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..."); }
}
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();
}
}
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();
}
}
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!
This content originally appeared on DEV Community and was authored by André Moriya