SOLID Principles
Why Design Principles?
Software evolves: teams, requirements, and deadlines change.
Codebases grow into massive systems.
Small design mistakes lead to long-term maintenance issues.
Systems become rigid, fragile, and expensive to change.
Without good design:
Adding features becomes dangerous.
Fixing bugs introduces new ones.
Developers fear touching old code.
Symptoms of Bad Design
Rigidity: Difficult to change.
Fragility: Easy to break.
Immobility: Hard to reuse.
Viscosity: Easier to hack than do it properly.
Opacity: Hard to understand the code.
Design rot is real, and SOLID helps fight it!
Real-World Analogy: Restaurant Kitchen
Bad Design:
Ingredients thrown into one fridge.
Recipes are handwritten differently every day.
Everyone shouts for ingredients.
Results:
Meals take forever.
Orders are wrong.
Unhappy customers.
Good Design:
Ingredients are labeled and stored correctly.
Recipes are standardized.
Chefs know their roles.
Good Code:
Clear organization.
Responsibilities are divided.
Everyone (and every object) knows their role.
SOLID principles = Recipes for building a clean kitchen for your code!
A Brief History of SOLID
Introduced by Robert C. Martin (Uncle Bob) around 2000.
Named and popularized by Michael Feathers.
Built on decades of object-oriented design experience.
Focused on writing code that is flexible, extensible, and maintainable.
Today:
SOLID principles are a foundation for Agile development.
Critical for Test-Driven Development (TDD), Design Patterns, and Clean Architecture.
The Five SOLID Principles
SOLID is an acronym:
S – Single Responsibility Principle (SRP)
O – Open/Closed Principle (OCP)
L – Liskov Substitution Principle (LSP)
I – Interface Segregation Principle (ISP)
D – Dependency Inversion Principle (DIP)
Single Responsibility Principle (SRP)
Official Definition: "A class should have one, and only one, reason to change."
Robert C. Martin ("Uncle Bob"), Agile Software Development.
Classes with multiple responsibilities are more fragile and harder to maintain.
Each class should only do one job.
If a class handles many jobs, changing one job might break another.
One actor, one responsibility.
Real-World Analogy: The Actor Principle
In a play, each actor plays one character.
A class should be responsible for just one role; don't mix unrelated responsibilities.
Bad Example: Employee Class
class Employee {
void calculatePay() { ... }
void saveToDatabase() { ... }
void generateReport() { ... }
}
Problem: Business logic (salary calculation), persistence logic (database save), and presentation logic (report generation) are all mixed together.
Problems When SRP is Violated
Tight Coupling: Change in database affects business logic.
Higher Risk: Bug fixes introduce unexpected side-effects.
Hard Testing: Unit testing is complicated because responsibilities are tangled.
Hard Reuse: You can’t reuse parts without dragging unrelated code.
SRP violation leads to fragile, tangled systems.
How to Apply SRP
Split responsibilities into separate classes:
One class per role.
Clear boundaries of responsibility.
Changes in one area don’t affect others.
Design Tip: Whenever you see ”and” in a class description, it probably needs a split!
Good Example: Separate Classes
class Employee { ... }
class PayCalculator {
double calculatePay(Employee e) { ... }
}
class EmployeeRepository {
void save(Employee e) { ... }
}
class EmployeeReport {
void generate(Employee e) { ... }
}
Each class now has one reason to change.
Checklist: Signs SRP Might Be Violated
Class does many unrelated things.
Class grows too large over time (”God Class”).
Class touches too many external systems (DB, UI, Email, etc.).
Multiple teams need to modify the same class for different reasons.
When in doubt: Split it out!
Mini-Quiz - SRP Practice
Which classes violate SRP? (Select all that apply)
A class that manages database connections and user sessions.
A class that only calculates invoice totals.
A class that reads files, writes to network, and processes payments.
A class that validates email formats.
Answer: 1 and 3 violate SRP.
Open/Closed Principle (OCP)
Official Definition: ”Software entities should be open for extension, but closed for modification.”
Meaning: Add new behavior without changing existing code.
Protect working code from being broken by changes.
Extend the behavior of a class without altering its source code.
Existing tested code should stay untouched as much as possible.
Build systems like LEGO blocks — add more pieces without reshaping old ones!
Real-World Analogy: Power Socket Extensions
Wall sockets are ”closed” — you don’t modify the wall wiring.
But you can ”extend” functionality — plug in extension cords, splitters, adapters.
In Code: Core modules stay stable. New features plug in without breaking old ones.
Bad Example: Graphic Editor with Switch Case
class GraphicEditor {
void drawShape(Shape s) {
if (s.type == ”Circle”) drawCircle(s);
else if (s.type == ”Square”) drawSquare(s);
}
}
Adding new shapes requires editing this method — violating OCP!
Problems When OCP is Violated
Fragile: changing switch logic may break unrelated shapes.
Risky: one typo could crash the system.
Inefficient: recompile and retest old working code every time.
Bottleneck: one team must control all modifications.
Bad for scalability and team growth.
How to Apply OCP
Use polymorphism:
Create an interface or abstract class.
Let each new behavior implement or inherit.
Core classes call the abstract methods—don't care about details.
This way, new behaviors come as new classes — no modification needed.
Good Example: Using Polymorphism
interface Shape {
void draw();
}
class Circle implements Shape {
public void draw() { /* draw circle */ }
}
class Square implements Shape {
public void draw() { /* draw square */ }
}
class GraphicEditor {
void drawShape(Shape s) {
s.draw();
}
}
New shapes extend the system—without touching GraphicEditor!
Checklist: Signs OCP Might Be Violated
Frequent modifications to stable code when adding new features.
Growing long ”if-else” or ”switch-case” chains.
Risk of introducing bugs in old logic when adding new options.
New types require editing core processing classes.
When in doubt: Abstract it out!
Mini-Quiz — OCP Practice
Which design follows OCP?
A switch-case for every new payment method (Visa, MasterCard, PayPal).
A Payment interface with classes for Visa, MasterCard, PayPal implementing it.
A hardcoded if-else for each notification type (Email, SMS, Push).
Answer: 2 follows OCP.
Liskov Substitution Principle (LSP)
Official Definition: ”Subtypes must be substitutable for their base types without altering desirable behavior.”
Introduced by: Barbara Liskov, 1987.
If code works with a base class, it should also work with any derived class—without surprises.
Subclasses must honor the promises made by their parents.
Child objects should behave like parents—or better.
Real-World Analogy: Vehicle Rental
You rent a ”vehicle” expecting to drive.
If the rental company gives you a ”boat” instead, you’re stuck!
In Code: Substituting types should not break client expectations.
Bad Example: Rectangle and Square
class Rectangle {
void setWidth(int w) { ... }
void setHeight(int h) { ... }
}
class Square extends Rectangle {
void setWidth(int w) {
super.setWidth(w);
super.setHeight(w);
}
void setHeight(int h) {
super.setWidth(h);
super.setHeight(h);
}
}
Square ”breaks” the behavior expected of Rectangle clients.
Problems When LSP is Violated
Clients get unpredictable behavior.
Testing becomes complicated.
Bugs are introduced silently—harder to detect.
Violation destroys confidence in type hierarchy!
How to Apply LSP
Redesign the hierarchy carefully.
Do not use inheritance if behavior cannot be preserved.
Prefer interfaces or separate abstractions if substitution doesn’t make sense.
Tip: Inheritance = ”is-a” relationship—check it carefully.
Good Example: No Forced Inheritance
interface Shape {
int area();
}
class Rectangle implements Shape {
int width, height;
int area() {
return width * height;
}
}
class Square implements Shape {
int side;
int area() {
return side * side;
}
}
Both shapes implement the same contract without confusion.
Checklist: Signs LSP Might Be Violated
Subclass overrides methods and changes expected behavior.
Subclass throws unexpected exceptions.
Clients must add ”instanceof” checks to distinguish subclasses.
When in doubt: Flatten the hierarchy!
Mini-Quiz — LSP Practice
Which one violates LSP?
A Dog class extends Animal and behaves correctly.
A Duck class extends Bird but cannot fly (throws exception when fly() is called).
A Car class implements Driveable and drive s safely.
Answer: 2 violates LSP.
Interface Segregation Principle (ISP)
Official Definition: ”Clients should not be forced to depend upon interfaces that they do not use.”
Meaning: Prefer many small, focused interfaces over one large general-purpose interface.
Clients should only know about the methods that are relevant to them.
Don’t burden classes with unnecessary obligations.
Don’t make a printer implement ”fax” if it can’t fax!
Real-World Analogy: Restaurant Menu
Imagine a vegetarian ordering from a menu that forces them to choose a meat dish.
Very confusing and annoying!
In Code: Clients should only see the methods they need.
Bad Example: Multi-Function Interface
interface Machine {
void print();
void scan();
void fax();
}
class BasicPrinter implements Machine {
public void print() { ... }
public void scan() {
throw new UnsupportedOperationException();
}
public void fax() {
throw new UnsupportedOperationException();
}
}
Classes are forced to implement methods they don’t support!
Problems When ISP is Violated
Unnecessary code complexity.
Higher risk of runtime errors (e.g., unsupported methods).
Harder to understand and maintain.
Break interfaces into smaller, focused roles!
How to Apply ISP
Split interfaces by responsibilities:
One interface per logical group.
Classes implement only what they actually support.
Example: Printer, Scanner, and Fax interfaces separately.
Good Example: Focused Interfaces
interface Printer {
void print();
}
interface Scanner {
void scan();
}
interface Fax {
void fax();
}
class BasicPrinter implements Printer {
public void print() { ... }
}
Classes now only implement what they truly offer!
Checklist: Signs ISP Might Be Violated
Classes throw exceptions for unimplemented methods.
Interfaces seem bloated or unrelated.
Clients know too much about unrelated behavior.
When in doubt: Split it out!
Mini-Quiz — ISP Practice
Which option follows ISP?
A Device interface with print(), scan(), fax(), copy() methods.
Separate Printer, Scanner, Copier interfaces.
Answer: 2 follows ISP.
Dependency Inversion Principle (DIP)
Official Definition: ”High-level modules should not depend on low-level modules. Both should depend on abstractions.”
Also: ”Abstractions should not depend on details. Details should depend on abstractions.”
Code should depend on interfaces, not implementations.
High-level logic (business rules) should not know how low-level parts (e.g., database, email) work.
Don’t hardcode dependencies; inject flexibility.
Real-World Analogy: Universal Power Adapters
Travelers use universal adapters — they work regardless of country.
You plug into a standard interface, not directly into the wall’s wiring.
In Code: Design against interfaces, not hardcoded classes.
Bad Example: Hardcoded Dependency
class EmailSender {
void send(String message) { ... }
}
class OrderService {
private EmailSender sender = new EmailSender();
void completeOrder() {
sender.send(”Order completed”);
}
}
Problem: Can’t reuse OrderService with a different message sender. Hard to test (no mocks).
Problems When DIP is Violated
Low-level changes ripple into high-level business logic.
Difficult to replace components (e.g., swap database or logger).
Harder to test in isolation.
Leads to tight coupling between layers.
Abstract away your dependencies!
How to Apply DIP
Solution: Introduce an interface or abstract base class.
Make high-level modules depend on that abstraction.
Inject the concrete implementation at runtime (via constructor).
Use with: IoC, Dependency Injection, Mocking for tests.
Good Example: Abstracted Dependency
interface Notifier {
void send(String message);
}
class EmailSender implements Notifier {
public void send(String message) { ... }
}
class OrderService {
private final Notifier notifier;
OrderService(Notifier notifier) {
this.notifier = notifier;
}
void completeOrder() {
notifier.send(”Order completed”);
}
}
Checklist: Signs DIP Might Be Violated
Business logic instantiates low-level classes directly.
Cannot easily switch implementations (e.g., File → DB → Cloud).
Unit tests are difficult due to hardwired dependencies.
Code breaks if low-level modules change their details.
When in doubt: Invert the dependency!
Mini-Quiz — DIP Practice
Which example follows DIP?
LoggerService creates a FileLogger directly in its constructor.
LoggerService accepts an ILogger interface via constructor.
Answer: 2 follows DIP.
SOLID — Quick Recap
S — Single Responsibility Principle: Each class should have only one reason to change.
O — Open/Closed Principle: Classes should be open for extension, closed for modification.
L — Liskov Substitution Principle: Subtypes must be usable in place of their supertypes without altering behavior.
I — Interface Segregation Principle: Many client-specific interfaces are better than one general-purpose interface.
D — Dependency Inversion Principle: Depend on abstractions, not concretions.
Final Key Messages
Good design is deliberate, not accidental.
SOLID principles are guidelines, not rigid rules.
Prioritize clarity, simplicity, and separation of concerns.
Mastering SOLID leads to flexible, robust, and scalable systems.
Design today what you’ll be proud to maintain tomorrow!
Quick Quiz — Test Your Understanding
Which principle aims to minimize ”God Classes”? (Answer: SRP)
Which principle says software should be extendable without modifying existing code? (Answer: OCP)
Violating which principle leads to runtime surprises when substituting types? (Answer: LSP)
Which principle promotes using smaller, focused interfaces? (Answer: ISP)
Which principle recommends depending on interfaces rather than implementations? (Answer: DIP)
Discussion Questions
Give a real-world example where SRP violation caused a problem.
How would OCP help in building a plugin system?
How could LSP violations cause subtle bugs in polymorphic collections?
Mini Case Studies
SRP: Class
InvoiceManagerhandles invoice calculations, database saving, PDF generation, and emailing invoices.Identify how SRP is violated and suggest a refactoring.
OCP:
NotificationServiceuses a huge if-else ladder to send emails, SMS, and push notifications.How is OCP violated? How could you extend this system without modifying the service?
LSP: A
Birdclass has afly()method. APenguinsubclass throws an exception whenfly()is called.Why is LSP violated? How could you redesign this?
ISP: A
SmartDeviceinterface includescall(),browseInternet(),playGames(), but a basic feature-phone only supports calling.How is ISP violated? How would you redesign the interfaces?
DIP: An
OrderProcessorcreates aMySQLDatabaseobject directly inside its methods.Why does this violate DIP? Suggest a better design.