███████╗ ██████╗ ██╗ ██╗██████╗
██╔════╝██╔═══██╗██║ ██║██╔══██╗
███████╗██║ ██║██║ ██║██║ ██║
╚════██║██║ ██║██║ ██║██║ ██║
███████║╚██████╔╝███████╗██║██████╔╝
╚══════╝ ╚═════╝ ╚══════╝╚═╝╚═════╝
src/
├── srp/ ← Single Responsibility Principle
│ ├── bad/
│ │ ├── UserBad.java
│ │ └── MainSRPBad.java
│ └── good/
│ ├── User.java
│ ├── UserRepository.java
│ ├── EmailService.java
│ ├── UserReportGenerator.java
│ └── MainSRPGood.java
├── ocp/ ← Open/Closed Principle
│ ├── bad/
│ │ ├── AreaCalculatorBad.java
│ │ └── MainOCPBad.java
│ └── good/
│ ├── Shape.java
│ ├── Circle.java
│ ├── Rectangle.java
│ ├── Triangle.java
│ ├── AreaCalculator.java
│ └── MainOCPGood.java
├── lsp/ ← Liskov Substitution Principle
│ ├── bad/
│ │ ├── RectangleBad.java
│ │ ├── SquareBad.java
│ │ └── MainLSPBad.java
│ └── good/
│ ├── Shape.java
│ ├── Rectangle.java
│ ├── Square.java
│ └── MainLSPGood.java
├── isp/ ← Interface Segregation Principle
│ ├── bad/
│ │ ├── Worker.java
│ │ ├── HumanWorkerBad.java
│ │ ├── RobotWorkerBad.java
│ │ └── MainISPBad.java
│ └── good/
│ ├── Workable.java
│ ├── Feedable.java
│ ├── Sleepable.java
│ ├── HumanWorker.java
│ ├── RobotWorker.java
│ └── MainISPGood.java
└── dip/ ← Dependency Inversion Principle
├── bad/
│ ├── MySQLDatabase.java
│ ├── UserServiceBad.java
│ └── MainDIPBad.java
└── good/
├── Database.java
├── MySQLDatabase.java
├── MongoDatabase.java
├── InMemoryDatabase.java
├── UserService.java
└── MainDIPGood.java
Each principle folder contains a
bad/sub-package showing the violation and agood/sub-package showing the correct approach — both with a runnableMain.
"A class should have only one reason to change."
A class that does too much is hard to maintain. When the DB logic, email logic, and report format all live in the same file, changing any one of them risks breaking the others.
| Status | File | Responsibility |
|---|---|---|
| ❌ | srp/bad/UserBad.java |
Handles user data + DB + email + reports — all in one |
| ✅ | srp/good/User.java |
Holds only user data |
| ✅ | srp/good/UserRepository.java |
Handles only DB persistence |
| ✅ | srp/good/EmailService.java |
Handles only email sending |
| ✅ | srp/good/UserReportGenerator.java |
Handles only report generation |
Show key insight
❌ UserBad
├── saveToDatabase() ← DB concern
├── sendWelcomeEmail() ← Email concern
└── generateReport() ← Report concern
— Change email template → must edit User class. Wrong.
✅ User → data only
UserRepository → DB only Each class has
EmailService → email only one reason
UserReportGenerator → reports to change.
"Open for extension, closed for modification."
When adding a new shape requires editing the calculator class, you risk breaking existing logic. The fix: define an abstraction and let each shape own its behaviour.
| Status | File | Description |
|---|---|---|
| ❌ | ocp/bad/AreaCalculatorBad.java |
if/else instanceof chain — breaks every time a new shape is added |
| ✅ | ocp/good/Shape.java |
Interface that all shapes implement |
| ✅ | ocp/good/Circle.java · Rectangle.java · Triangle.java |
Each shape owns its area() logic |
| ✅ | ocp/good/AreaCalculator.java |
Works with any Shape — never needs to change |
Show key insight
❌ calculateArea(Object shape) {
if (shape instanceof Circle) { ... }
else if (shape instanceof Rectangle) { ... }
// ← must open this file to add Triangle
}
✅ interface Shape { double area(); }
class Triangle implements Shape {
public double area() { return 0.5 * base * height; }
}
// AreaCalculator untouched ✓
"Subtypes must be substitutable for their base types without altering the correctness of the program."
A Square that silently changes both sides when you set the width is not a proper Rectangle. Substituting it causes silent, wrong results.
| Status | File | Description |
|---|---|---|
| ❌ | lsp/bad/SquareBad.java |
Extends Rectangle but overrides setters to keep sides equal — breaks the area contract |
| ❌ | lsp/bad/RectangleBad.java |
Base whose contract is violated by SquareBad |
| ✅ | lsp/good/Shape.java |
Common abstract base — Rectangle and Square are independent |
| ✅ | lsp/good/Rectangle.java · Square.java |
Each models its geometry correctly — safe to substitute |
Show key insight
❌ rect.setWidth(4);
rect.setHeight(5);
// Expected: 20 — Got: 25 (if rect is actually a SquareBad)
✅ List<Shape> shapes = List.of(new Rectangle(4,5), new Square(5));
shapes.forEach(s -> s.area()); // Always correct ✓
"Clients should not be forced to depend on interfaces they do not use."
A fat interface forces RobotWorker to implement eat() and sleep() — methods that make no sense for a machine — leading to empty stubs or runtime exceptions.
| Status | File | Description |
|---|---|---|
| ❌ | isp/bad/Worker.java |
Fat interface: work() + eat() + sleep() — all or nothing |
| ❌ | isp/bad/RobotWorkerBad.java |
Forced to throw UnsupportedOperationException for eat() and sleep() |
| ✅ | isp/good/Workable.java |
Focused interface — just work() |
| ✅ | isp/good/Feedable.java |
Focused interface — just eat() |
| ✅ | isp/good/Sleepable.java |
Focused interface — just sleep() |
| ✅ | isp/good/RobotWorker.java |
Implements only Workable — no stubs, no surprises |
Show key insight
❌ class RobotWorkerBad implements Worker {
public void eat() { throw new UnsupportedOperationException(); }
public void sleep() { throw new UnsupportedOperationException(); }
}
✅ class RobotWorker implements Workable {
public void work() { System.out.println("Robot working..."); }
// Done. Nothing else needed.
}
"Depend on abstractions, not concretions. High-level modules should not depend on low-level modules."
Hardcoding new MySQLDatabase() inside business logic couples your service to a specific technology. Swapping to MongoDB — or even mocking for tests — requires editing the business class itself.
| Status | File | Description |
|---|---|---|
| ❌ | dip/bad/UserServiceBad.java |
Hardcodes new MySQLDatabase() — impossible to swap or test |
| ✅ | dip/good/Database.java |
Abstraction both layers depend on |
| ✅ | dip/good/UserService.java |
Receives any Database via constructor injection |
| ✅ | dip/good/MySQLDatabase.java · MongoDatabase.java |
Concrete implementations — swappable at the call site |
| ✅ | dip/good/InMemoryDatabase.java |
Drop-in for fast, isolated unit tests — no real DB needed |
Show key insight
❌ class UserServiceBad {
private MySQLDatabase db = new MySQLDatabase(); // hardwired
}
✅ class UserService {
private final Database db;
public UserService(Database db) { this.db = db; } // injected
}
// At runtime: new UserService(new MySQLDatabase())
// In tests: new UserService(new InMemoryDatabase()) ✓
Every bad/ and good/ folder has its own Main. Run them side by side to see the contrast.
# ─── SRP ───────────────────────────────────────────
javac srp/bad/*.java && java srp.bad.MainSRPBad
javac srp/good/*.java && java srp.good.MainSRPGood
# ─── OCP ───────────────────────────────────────────
javac ocp/bad/*.java && java ocp.bad.MainOCPBad
javac ocp/good/*.java && java ocp.good.MainOCPGood
# ─── LSP ───────────────────────────────────────────
javac lsp/bad/*.java && java lsp.bad.MainLSPBad
javac lsp/good/*.java && java lsp.good.MainLSPGood
# ─── ISP ───────────────────────────────────────────
javac isp/bad/*.java && java isp.bad.MainISPBad
javac isp/good/*.java && java isp.good.MainISPGood
# ─── DIP ───────────────────────────────────────────
javac dip/bad/*.java && java dip.bad.MainDIPBad
javac dip/good/*.java && java dip.good.MainDIPGood💡 Or simply open the project in IntelliJ IDEA or VS Code and run any
Mainclass directly with the▶️ gutter button.
Made with ☕ by Mazennaji