In the previous post, we covered:
- SRP (keep classes focused)
- OCP (extend without modifying)
Those help you structure code well.
But even with those applied, systems can still break in subtle ways:
- inheritance behaving unexpectedly
- classes forced to implement irrelevant methods
- tight coupling making changes painful
That’s where the remaining SOLID principles come in:
- LSP (Liskov Substitution Principle)
- ISP (Interface Segregation Principle)
- DIP (Dependency Inversion Principle)
LSP — Liskov Substitution Principle
Subclasses should be replaceable for their base classes without breaking behavior.
The classic mistake
Bird:
- fly()
Penguin extends Bird:
- fly() → throws error
This design assumes:
All birds can fly
But penguins cannot.
Now, anywhere Bird is used, substituting it with Penguin breaks the system.
What actually went wrong
The issue is not inheritance.
The issue is:
The base class was designed incorrectly.
Correct approach
Bird:
- eat()
- make_sound()
FlyingBird:
- fly()
Now:
- Sparrow → Bird + FlyingBird
- Penguin → Bird
No invalid behavior. No surprises.
Key insight
If a subclass cannot fully support a method, the abstraction is wrong.
ISP — Interface Segregation Principle
Clients should not be forced to depend on methods they don’t use.
The common mistake
Device:
- print()
- scan()
- fax()
Now:
- A simple printer must implement scan and fax
- A scanner must implement print
This leads to:
- unused methods
- dummy implementations
- confusion in design
Correct approach
Split interfaces:
Printable → print()
Scannable → scan()
Faxable → fax()
Now:
- Printer → Printable
- Scanner → Scannable
- All-in-one → Printable + Scannable + Faxable
Each class implements only what it needs.
Key insight
Smaller, focused interfaces lead to cleaner and more flexible systems.
DIP — Dependency Inversion Principle
High-level modules should not depend on low-level modules. Both should depend on abstractions.
The common mistake
NotificationService → EmailSender
This creates tight coupling.
Problems:
- Switching to SMS requires code changes
- Adding new channels becomes harder
Correct approach
Introduce abstraction:
Notification:
- send()
EmailNotification → send()
SMSNotification → send()
PushNotification → send()
Now:
NotificationService:
- notify(notification)
The service doesn’t care about implementation details.
Important design detail
Who decides which notification to send?
The caller (e.g., OrderService)
This keeps:
- NotificationService simple
- System extensible
Key insight
Depend on behavior (interfaces), not concrete implementations.
Bringing it all together
These three principles solve different kinds of design problems:
- LSP → prevents broken inheritance
- ISP → prevents bloated interfaces
- DIP → prevents tight coupling
Combined with SRP and OCP, they give you:
A system that is stable, flexible, and easy to extend
A practical way to remember
When designing, ask:
- Will this subclass behave correctly? (LSP)
- Am I forcing unnecessary methods? (ISP)
- Am I tightly coupled to implementations? (DIP)
Closing thought
Most design issues don’t show up immediately.
They appear when systems evolve.
SOLID principles help you design not just for today, but for change over time.
Top comments (0)