Before moving into the DRY principle, let's understand the goals of a good programmer:
- Solving Problems
- Writing Amazing Code
- Maintainsability
- Simplicity
- Cleanliness
- Optimization
🤔 Why write clean, simple, and maintainable code?
Because you're writing code for people — not just machines.
- Your team members need to understand your code.
- When you return to your own code months later, it should still make sense.
Clean code = Better teamwork + Fewer bugs + Easier updates
❓ Is there a standard way to write clean code?
Yes! Here are some principles:
- DRY (Don't Repeat Yourself)
- KISS (Keep It Simple, Stupid)
- YAGNI (You Aren’t Gonna Need It)
- SOLID (5 key principles for object-oriented programming)
🧱 What is SOLID?
SOLID is a set of five design principles that make your code:
- Easy to read
- Easy to maintain
- Easy to scale and extend
These principles were introduced by Robert C. Martin (Uncle Bob) in the early 2000s, and have become a cornerstone of clean software architecture.
💁♂️ What Does SOLID Stand For?
- S – Single Responsibility Principle
- O – Open/Closed Principle
- L – Liskov Substitution Principle
- I – Interface Segregation Principle
- D – Dependency Inversion Principle
We'll explore each of these with real-world examples and simple code.
Let’s get started! 🚀
S: Single Responsibility Principle (SRP)
A class should have only one reason to change.
This means every class should have only one responsibility or one job.
✅ Why is SRP Important?
When a class has just one task, it:
- Becomes easier to understand
- Is more maintainable
- Contains fewer bugs
- Can be updated or modified without affecting unrelated features
On the other hand, if a class has multiple responsibilities, it:
- Becomes confusing
- Is harder to debug
- Increases risk when making changes
❌ Violating SRP (Bad Design)
Imagine a baker in a bakery.
If that one baker is responsible for:
- Baking bread
- Managing inventory
- Cleaning the bakery
Then their focus is split, and none of the tasks are done with high quality.
class Baker {
public:
void bakeBread() {
cout << "Baking bread..." << endl;
}
void manageInventory() {
cout << "Managing inventory..." << endl;
}
void cleanBakery() {
cout << "Cleaning the bakery..." << endl;
}
};
int main() {
Baker baker;
baker.bakeBread();
baker.manageInventory();
baker.cleanBakery();
}
- Here, the
Baker
class has 3 responsibilities. - So if we want to change the cleaning process, the
Baker
class must be modified — even though it's not related to baking!
✅ Follows SRP (Good Design)
Imagine a baker in a bakery.
If that one baker is responsible for:
- Baking bread
Then their focus is on a single task, and they will perform that task with high quality.
class BreadBaker {
public:
void bakeBread() {
cout << "Baking bread..." << endl;
}
};
class InventoryManager {
public:
void manageInventory() {
cout << "Managing inventory..." << endl;
}
};
class BakeryCleaner {
public:
void cleanBakery() {
cout << "Cleaning the bakery..." << endl;
}
};
int main() {
BreadBaker baker;
baker.bakeBread();
InventoryManager manager;
manager.manageInventory();
BakeryCleaner cleaner;
cleaner.cleanBakery();
}
Now:
- Each class now has a single responsibility:
-
BreadBaker
only bakes bread -
InventoryManager
only manages inventory -
BakeryCleaner
only cleans
-
- If we want to change how inventory is managed, we only update the
InventoryManager
class. - The code is clean, modular, and easy to maintain.
O: Open/Closed Principle (OCP)
Software entities like classes, modules, and functions should be open for extension but closed for modification.
This means you can add new features by extending existing code, but you should not modify the already working code.
✅ Why is OCP Important?
- Prevents breaking existing code
- Encourages reusable components
- Improves code maintainability
❌ Violating OCP (Bad Design)
Imagine we have a Shape
class and we defined Circle
and Rectangle
. Now imagine if a new shape (like Triangle
) is introduced — we will have to modify the existing class, which violates the Open/Closed Principle.
class Shape {
public:
string type;
double calculateArea() {
if (type == "circle") {
// calculate circle area
return 3.14 * 5 * 5; // assume radius = 5
}
else if (type == "rectangle") {
// calculate rectangle area
return 10 * 20; // assume width = 10, height = 20
}
}
};
int main() {
Shape shape;
shape.type = "circle";
cout << shape.calculateArea();
}
- This class is not closed for modification — every time we add a new
shape
liketriangle
, we must edit theShape
class. - This makes the code fragile and harder to maintain.
- It violates OCP, because the class is not extensible without changing existing code.
✅ Follows OCP (Good Design)
Let’s fix this using inheritance and polymorphism - we extend functionality without touching the base class.
class Shape {
public:
virtual double calculateArea() = 0;
};
class Circle : public Shape {
public:
double radius;
Circle(double r) : radius(r) {}
double calculateArea() override {
return 3.14 * radius * radius;
}
};
class Rectangle : public Shape {
public:
double width, height;
Rectangle(double w, double h) : width(w), height(h) {}
double calculateArea() override {
return width * height;
}
};
int main() {
Shape* shape1 = new Circle(5);
cout << "Circle Area: " << shape1->calculateArea() << endl;
Shape* shape2 = new Rectangle(10, 20);
cout << "Rectangle Area: " << shape2->calculateArea() << endl;
delete shape1;
delete shape2;
}
- Now the
Shape
class is closed for modification but open for extension. - We can add new shapes (like Triangle, Square, etc.) by just creating new classes that implement
calculateArea()
. - This approach follows the Open/Closed Principle:
- Open for extension ✅
- Closed for modification ✅
L: Liskov Substitution Principle (LSP)
Introduced by Barbara Liskov in 1987
"Objects of a superclass should be replaceable with objects of its subclasses without breaking the application."
🧠 What it Means
If Class B
is a subclass of Class A
, then we should be able to use B
wherever we use A
without altering the correctness of the program.
- ✅ A subclass must respect the behavior of its parent.
- ❌ If a subclass breaks the behavior expected from the parent, it violates LSP.
✅ Why is LSP Important?
- Ensures reliability when using polymorphism.
- Avoids unexpected behaviors in subclass implementation.
❌ Violating LSP (Bad Design)
Imagine a base class Vehicle
with a method startEngine()
. Now we have two subclasses: Car
and Bicycle
.
class Vehicle {
public:
virtual void startEngine() {
cout << "Engine started!" << endl;
}
};
class Car : public Vehicle {
public:
void startEngine() override {
cout << "Car engine started!" << endl;
}
};
class Bicycle : public Vehicle {
public:
void startEngine() override {
// But bicycles don't have engines!
// This leads to misleading behavior or unnecessary implementation
throw logic_error("Bicycles don't have engines!");
}
};
int main() {
Vehicle* v1 = new Car();
v1->startEngine(); // ✅ Fine
Vehicle* v2 = new Bicycle();
v2->startEngine(); // ❌ Throws error — Violates LSP
delete v1;
delete v2;
}
- The
Bicycle
class inheritsstartEngine()
, which it should not because bicycles don’t have engines. - This forces
Bicycle
to implement a method it logically doesn’t need. -
Breaks LSP: Subclass
Bicycle
cannot fully substituteVehicle
.
✅ Follows LSP (Good Design)
Split responsibilities — create separate hierarchies for engine and non-engine vehicles.
class Vehicle {
public:
virtual void move() = 0;
};
class EngineVehicle : public Vehicle {
public:
virtual void startEngine() = 0;
};
class NonEngineVehicle : public Vehicle {
// No startEngine() here
};
class Car : public EngineVehicle {
public:
void startEngine() override {
cout << "Car engine started!" << endl;
}
void move() override {
cout << "Car is moving!" << endl;
}
};
class Bicycle : public NonEngineVehicle {
public:
void move() override {
cout << "Bicycle is moving!" << endl;
}
};
int main() {
EngineVehicle* car = new Car();
car->startEngine(); // ✅ Valid
car->move();
NonEngineVehicle* bicycle = new Bicycle();
bicycle->move(); // ✅ No engine needed
delete car;
delete bicycle;
}
- Now, no subclass is forced to implement irrelevant methods.
- Each subclass respects the behavior expected from its parent.
- LSP is satisfied — you can replace any parent type with its subclass without breaking the system.
I: Interface Segregation Principle (ISP)
Applicable to interfaces rather than classes.
Segregates interfaces based on functionality.
Do not force any client to implement an interface that is not relevant to them.
🧠 Why is ISP Important?
- Reduces unnecessary dependencies.
- Simplifies implementation for specific use cases.
- Ensures that classes only implement the methods they actually need, leading to more efficient and maintainable code.
❌ Violating ISP (Bad Design)
Imagine a Machine
interface that has three methods: print()
, scan()
, and fax()
. Now, if we have a BasicPrinter
class, it is forced to implement methods like scan()
and fax()
, even though they are not relevant to a basic printer.
interface Machine {
void print();
void scan();
void fax();
}
class AllInOnePrinter : public Machine {
public:
void print() override {
// Implement printing
}
void scan() override {
// Implement scanning
}
void fax() override {
// Implement faxing
}
};
class BasicPrinter : public Machine {
public:
void print() override {
// Implement printing
}
void scan() override {
// Can't scan, so we throw an error or leave it empty
}
void fax() override {
// Can't fax, so we throw an error or leave it empty
}
};
- The
BasicPrinter
class has to implementscan()
andfax()
, even though it doesn't need them. - This design flaw makes the code less flexible and harder to extend.
- If new functionality is added to the
Machine
interface, every class that implements it needs to implement the new methods — even if they don't need them.
✅ Adhering to ISP (Good Design)
To follow the Interface Segregation Principle, we split the Machine
interface into smaller, more focused interfaces
, so classes only implement the methods they need.
interface Printer {
void print();
}
interface Scanner {
void scan();
}
interface FaxMachine {
void fax();
}
class BasicPrinter : public Printer {
public:
void print() override {
// Implement printing
}
};
class AllInOnePrinter : public Printer, public Scanner, public FaxMachine {
public:
void print() override {
// Implement printing
}
void scan() override {
// Implement scanning
}
void fax() override {
// Implement faxing
}
};
- This design follows the principle of "interface cohesion": Each interface has a specific responsibility, which leads to better cohesion and separation of concerns.
Now, the
BasicPrinter
only implementsPrinter
, and theAllInOnePrinter
implementsPrinter
,Scanner
, and FaxMachine — only the relevant methods.The result is a more robust, flexible, and maintainable system.
D: Dependency Inversion Principle (DIP)
Depend on abstractions, not on concrete classes.
- High-level modules should not depend on low-level modules.
- Both should depend on abstractions (e.g., interfaces or abstract classes).
- Abstractions should not depend on details.
- Details should depend on abstractions.
🧠 What DIP Means
Classes should depend on abstractions, not on concrete classes.
- ❌ Bad: High-level modules directly use concrete implementations.
- ✅ Good: High-level modules use interfaces, and low-level modules implement those interfaces.
✅ Why is DIP Important?
- Promotes decoupled architecture
- Makes the system easier to test
- Facilitates changes and scalability
- Improves flexibility and maintainability
❌ Violating DIP (Bad Design)
Imagine an OrderService
that directly creates and uses an EmailNotifier
. This makes it tightly dependent on a specific notification method.
class EmailNotifier {
public:
void sendEmail() {
cout << "Sending Email\n";
}
};
class OrderService {
EmailNotifier notifier;
public:
void placeOrder() {
// Directly using concrete class
notifier.sendEmail();
}
};
int main() {
OrderService order;
order.placeOrder(); // ❌ Tightly coupled
}
-
OrderService
is tightly coupled withEmailNotifier
. - Cannot switch to SMS or other notifications easily.
✅ Following DIP (Good Design)
- To fix this, we introduce a
Notifier
interface. NowOrderService
depends on an abstraction, making it flexible and decoupled from concrete implementations.
// Abstraction
class Notifier {
public:
virtual void send() = 0;
};
// Concrete implementations
class EmailNotifier : public Notifier {
public:
void send() override {
cout << "Sending Email\n";
}
};
class SMSNotifier : public Notifier {
public:
void send() override {
cout << "Sending SMS\n";
}
};
// Depends on abstraction
class OrderService {
Notifier* notifier;
public:
OrderService(Notifier* n) : notifier(n) {}
void placeOrder() {
notifier->send(); // ✅ Uses interface, not concrete class
}
};
int main() {
EmailNotifier email;
OrderService order(&email);
order.placeOrder();
}
-
OrderService
works with anyNotifier
(Email, SMS, Push, etc.). - Loosely coupled and easy to test or extend.
💬 Thank you for reading!
If you found it valuable, hit a like ❤️ and share it with others!
Got questions or suggestions?
Drop a comment — I’d love to hear from you! 🙌