A solid explanation to SOLID Principle
Source: Dev.to
Introduction
A piece of software rarely fails because “the algorithm was wrong.”
More often it degrades slowly: features become hard to add, bugs re‑appear, and even a tiny change feels risky.
The SOLID principles give us five practical ways to write code that stays understandable and adaptable for a significantly longer period of time.
SOLID is not one of the three states of matter.
It is an acronym for five design principles that help us write highly maintainable code.
The principles are:
- Single Responsibility Principle (SRP)
- Open‑Closed Principle (OCP)
- Liskov Substitution Principle (LSP)
- Interface Segregation Principle (ISP)
- Dependency Inversion Principle (DIP)
Together, they encourage loose coupling, clear responsibilities, and stable abstractions—essential for long‑lived code.
1. Single Responsibility Principle (SRP)
A class should have one and only one reason to change.
Bad example
class UserService {
public void validate(User user) { /* … */ }
public void saveToDatabase(User user) { /* … */ }
public void sendEmail(User user) { /* … */ }
public void log(User user) { /* … */ }
}
Problems
- Four reasons to change (
validate,saveToDatabase,sendEmail,log). - Hard to reuse – callers don’t want to trigger email and DB writes when they only need validation.
- Testing nightmare – you must mock logger, email client, and DB client.
Better design
class UserValidator {
public void validate(User user) { /* … */ }
}
class UserRepository {
public void save(User user) { /* … */ }
}
class EmailNotifier {
public void send(User user) { /* … */ }
}
class Logger {
public void log(User user) { /* … */ }
}
Each class now has one reason to change and can be used independently (e.g., validation without emailing).
Mantra: Ek class – ek kaam (One class – one job).
2. Open‑Closed Principle (OCP)
Software entities should be open for extension but closed for modification.
Violating OCP
class AreaCalculator {
public double calculateArea(Object shape) {
if (shape instanceof Rectangle) {
Rectangle r = (Rectangle) shape;
return r.width * r.height;
} else if (shape instanceof Circle) {
Circle c = (Circle) shape;
return Math.PI * c.radius * c.radius;
}
// Adding a Square would require modifying this method → OCP violation
}
}
OCP‑compliant design
abstract class Shape {
abstract double area();
}
class Rectangle extends Shape {
double width, height;
@Override double area() { return width * height; }
}
class Circle extends Shape {
double radius;
@Override double area() { return Math.PI * radius * radius; }
}
// New shapes (e.g., Square) just extend Shape – no change to AreaCalculator
class AreaCalculator {
public double calculateArea(Shape shape) {
return shape.area();
}
}
Now we can add a Square (or any new shape) without touching AreaCalculator.
Mantra: Extend karo, edit nahi (Extend, don’t edit).
3. Liskov Substitution Principle (LSP)
Objects of a superclass shall be replaceable with objects of a subclass without altering the desirable properties of the program.
Problematic example
interface Bird {
void fly();
void walk();
}
class Sparrow implements Bird {
public void fly() { /* flying logic */ }
public void walk() { /* walking logic */ }
}
class Ostrich implements Bird {
public void fly() { throw new UnsupportedOperationException(); }
public void walk() { /* walking logic */ }
}
Ostrich breaks the contract of fly() – code that expects any Bird to fly will fail.
Better abstraction
interface Walkable {
void walk();
}
interface Flyable {
void fly();
}
abstract class Bird implements Walkable { /* common walk logic */ }
class Sparrow extends Bird implements Flyable {
public void fly() { /* flying logic */ }
}
class Ostrich extends Bird {
// No fly() method – respects LSP
}
Now makeWalk(Bird b) works for both Sparrow and Ostrich without unexpected behavior.
Memory aid (Hindi): Agar bachche ka behavior parents ke jaise hai, to wo party mein ja sakta hai (If a subclass behaves like its parent, it can be used interchangeably).
4. Interface Segregation Principle (ISP)
Clients should not be forced to depend on interfaces they do not use.
Violating ISP
interface IWorker {
void work();
void eat(); // Robots don’t eat!
}
A Robot class would have to implement a meaningless eat() method, creating dummy code and tight coupling.
ISP‑compliant design
interface Workable {
void work();
}
interface Eatable {
void eat();
}
class Human implements Workable, Eatable {
public void work() { /* … */ }
public void eat() { /* … */ }
}
class Robot implements Workable {
public void work() { /* … */ }
}
Now each class implements only the interfaces it actually needs.
Mantra: Divide and Rule – split large interfaces into focused ones.
5. Dependency Inversion Principle (DIP)
High‑level modules should not depend on low‑level modules; both should depend on abstractions.
Tight‑coupled example
class MasalaTeaMaker {
public String makeTea() { return "Masala Tea ready!"; }
}
class TeaShop {
private MasalaTeaMaker maker = new MasalaTeaMaker(); // Hard‑coded dependency!
public String orderTea(String type) {
if ("masala".equals(type)) {
return maker.makeTea();
}
return "Only Masala available"; // Rigid!
}
}
TeaShop is directly tied to MasalaTeaMaker. Adding a new tea type would require changing TeaShop.
DIP‑compliant design
interface TeaMaker {
String makeTea();
}
class MasalaTeaMaker implements TeaMaker {
public String makeTea() { return "Masala Tea ready!"; }
}
class GreenTeaMaker implements TeaMaker {
public String makeTea() { return "Green Tea ready!"; }
}
class TeaShop {
private final Map<String, TeaMaker> makers = new HashMap<>();
public TeaShop(Map<String, TeaMaker> makers) {
this.makers.putAll(makers);
}
public String orderTea(String type) {
TeaMaker maker = makers.get(type);
return (maker != null) ? maker.makeTea() : "Tea not available";
}
}
TeaShopdepends on the abstractionTeaMaker.- New tea makers can be added without modifying
TeaShop; just register them in the map.
Summary
| Principle | Core Idea | Typical Mantra |
|---|---|---|
| SRP | One reason to change per class | Ek class – ek kaam |
| OCP | Extend behavior without modifying existing code | Extend karo, edit nahi |
| LSP | Subtypes must be substitutable for their base types | Bachche ka behavior parents ke jaise |
| ISP | Split fat interfaces into client‑specific ones | Divide and Rule |
| DIP | Depend on abstractions, not concretions | High‑level ↔ low‑level via interfaces |
Applying these five principles helps us build loosely coupled, highly cohesive, and easily maintainable software that can evolve gracefully over time.
Updated TeaShop Example Using Abstraction
Below is a cleaned‑up version of the original snippet.
The code now follows the Dependency Inversion Principle (the “D” in SOLID) by depending on an abstraction (TeaMaker) instead of concrete classes.
1️⃣ Domain Model (Java)
// 1. Abstraction -------------------------------------------------
public interface TeaMaker {
String makeTea();
}
// 2. Concrete implementations --------------------------------------
public class MasalaTeaMaker implements TeaMaker {
@Override
public String makeTea() {
return "Masala Tea ready!";
}
}
public class GingerTeaMaker implements TeaMaker {
@Override
public String makeTea() {
return "Ginger Tea ready!";
}
}
// 3. High‑level module (depends on the abstraction) -------------
public class TeaShop {
// Constructor injection (setter injection is also possible)
private final TeaMaker teaMaker;
public TeaShop(TeaMaker teaMaker) {
this.teaMaker = teaMaker;
}
public String orderTea(String type) {
// The shop no longer knows about concrete makers.
// It simply asks the injected TeaMaker to prepare the drink.
return teaMaker.makeTea();
}
}
2️⃣ How It Works
| Component | Responsibility | Dependency |
|---|---|---|
TeaMaker | Defines the contract for any tea‑making class. | – |
MasalaTeaMaker / GingerTeaMaker | Concrete implementations that fulfil the TeaMaker contract. | Implements TeaMaker |
TeaShop | High‑level class that orders tea. | Depends only on TeaMaker (abstraction). |
The TeaShop no longer creates or knows about MasalaTeaMaker or GingerTeaMaker. Instead, a specific TeaMaker is supplied (injected) at runtime.
3️⃣ Dependency Injection Options
| Injection Type | Example |
|---|---|
| Constructor injection (shown above) | new TeaShop(new MasalaTeaMaker()) |
| Setter injection | java\npublic void setTeaMaker(TeaMaker teaMaker) { this.teaMaker = teaMaker; }\n |
| Field injection (framework‑specific) | @Inject private TeaMaker teaMaker; |
Choose the style that best fits your project’s architecture.
4️⃣ Quick Memory Aid
“Abstraction pe depend karo, class pe nahi.”
Think of it as “depend on the interface, not the concrete class.” This mantra keeps you aligned with SOLID’s Dependency Inversion Principle.
5️⃣ Closing Note
With the above refactor we have satisfied the SOLID principles, especially D (Dependency Inversion).
If you have any questions, feel free to drop a comment below.
Thanks for reading – stay tuned!