On this page
Description
Most code doesn't fail because it's wrong on day one. It fails because it becomes hard to change: every new feature touches five files, every fix breaks something unrelated, and nobody dares refactor. SOLID is five practical guidelines - collected by Robert C. Martin - for keeping object-oriented code easy to change. This skill is the map of all five; the next five skills take one principle each.
We'll go from simple to more technical:
- The five principles in one table, each with the kind of decay it prevents.
- One small class that shows three of the smells at once.
- How to use the principles as questions, not laws.
- When applying them makes code worse.
The five principles
Each principle guards against one specific way code decays:
| Principle | Short version | The decay it prevents |
|---|---|---|
| S - Single Responsibility | A class should have one reason to change. | Unrelated changes colliding in the same class. |
| O - Open/Closed | Extend behavior by adding code, not editing it. | Every new case editing (and re-testing) working code. |
| L - Liskov Substitution | Subtypes must be usable wherever the parent is. | Subclasses that surprise code written for the parent. |
| I - Interface Segregation | Many small interfaces beat one fat one. | Classes forced to implement methods they can't support. |
| D - Dependency Inversion | Depend on abstractions; inject the details. | Business logic welded to databases, APIs and frameworks. |
One example, three smells
Java
class InvoiceService {
double total(Invoice inv, String customerType) {
double t = inv.subtotal();
if (customerType.equals("VIP")) t *= 0.9; // O: a new customer type edits this method
else if (customerType.equals("STAFF")) t *= 0.7;
return t;
}
String toPdf(Invoice inv) { /* ... */ } // S: formatting lives here too...
void save(Invoice inv) {
new MySqlConnection("prod-db").insert(inv); // D: ...and a concrete database
}
}
Python
class InvoiceService:
def total(self, inv: Invoice, customer_type: str) -> float:
t = inv.subtotal()
if customer_type == "VIP": # O: a new customer type edits this method
t *= 0.9
elif customer_type == "STAFF":
t *= 0.7
return t
def to_pdf(self, inv: Invoice) -> bytes: ... # S: formatting lives here too...
def save(self, inv: Invoice) -> None:
MySqlConnection("prod-db").insert(inv) # D: ...and a concrete database
The code shows three of the smells (S, O and D). The other two need a hierarchy or an interface to appear: a subclass that throws UnsupportedOperationException (Python: NotImplementedError) for a method it inherited (L), and a client that must depend on a 25-method interface when it calls two of its methods (I). The five skills in this module each take one smell, explain the principle behind it, and practice the fix.
How to use the principles, step by step
- Treat them as diagnostic questions, not laws: "How many reasons does this class have to change?", "What do I have to edit to add a new case?"
- Apply them where change actually happens. A part of the code that never changes doesn't need extension points.
- Refactor towards them when a second or third variation appears - not in anticipation of ten.
- Use them together: DIP makes OCP possible, ISP keeps LSP achievable, SRP keeps classes small enough to reason about at all.
Which tool for which job?
| You notice | Ask | Principle |
|---|---|---|
| Unrelated changes keep landing in the same class | "Who asks for changes to this?" | Single Responsibility |
Every new case edits a working if/switch | "What must I edit to add a case?" | Open/Closed |
| A subclass throws for, or quietly changes, inherited behavior | "Can it stand in for its parent everywhere?" | Liskov Substitution |
| Implementations stub out methods they can't support | "Does every client need every method?" | Interface Segregation |
| Business logic constructs databases, clients or clocks itself | "Can I test this without the real thing?" | Dependency Inversion |
Common mistakes
| Mistake | What happens | Do this instead |
|---|---|---|
| Applying all five to every class from day one | twenty one-method classes and single-implementation interfaces | apply them where change actually happens |
| Adding extension points "just in case" | complexity for variations that never arrive | refactor when the second or third variation appears |
| Treating the principles as laws | arguments about rules instead of about the code | use them as diagnostic questions |
| Expecting SOLID to find the right abstractions | well-structured code around the wrong concepts | understand the domain first |
Check yourself
Try to answer before opening each one - recalling beats re-reading.
1. Which principle is violated when adding a new payment type means editing a working switch statement?
Show answer
Open/Closed - new behavior should be added by adding code (a new implementation of an interface), not by editing existing, tested code.
2. A NotificationService creates new SmtpClient() inside itself. Which principle does that ignore?
Show answer
Dependency Inversion - the high-level service should depend on an abstraction (MessageSender) that is injected, not construct a concrete detail itself.
3. Should you apply all five principles to every class from day one?
Show answer
No. Use them as diagnostics where the code actually changes; premature abstraction is its own maintenance cost.
Key takeaways
- SOLID = five guidelines for keeping code easy to change, each preventing one kind of decay.
- Use them as questions to ask about code, especially where change is frequent.
- Over-applying them creates needless indirection - balance with simplicity.
- They're coupling and cohesion rules, and they scale up to modules and services.