Design principles exist to answer one question:
When requirements change, does the system bend or break?
What are Design Principles
Design principles are guidelines, not rules.
They:
- Reduce coupling
- Improve readability
- Increase testability
- Enable safe refactoring
- Support long-term maintenance
Single Responsibility Principle (SRP)
A module should have one, and only one, reason to change.
SRP is about change reasons, not file size or class length.
Bad Example (Violation)
class Report {
public:
void generate();
void saveToFile();
void sendEmail();
};
Why this is bad:
- Changes in file storage break report logic
- Email changes affect report generation
- Testing becomes complex
Correct Design (SRP Applied)
class ReportGenerator {
public:
Report generate();
};
class ReportSaver {
public:
void save(const Report&);
};
class ReportMailer {
public:
void send(const Report&);
};
Benefits:
- Each class changes for one reason
- Easier testing
- Clear ownership
“How many different stakeholders can request changes to this class?”
If the answer is more than one → SRP violation.
Open / Closed Principle (OCP)
Software entities should be open for extension but closed for modification.
Bad Example
void processPayment(int type) {
if (type == 1) payByCard();
else if (type == 2) payByPaypal();
}
Adding a new payment method means editing existing code.
Correct Design
class PaymentMethod {
public:
virtual void pay() = 0;
};
class CardPayment : public PaymentMethod {
public:
void pay() override {}
};
class PaypalPayment : public PaymentMethod {
public:
void pay() override {}
};
Now you extend behavior without modifying existing logic.
New features should be **plug-ins**, not **edits**.
Liskov Substitution Principle (LSP)
Objects of a base class must be replaceable with objects of a derived class without breaking correctness.
Inheritance must preserve behavior expectations.
Bad Example
class Bird {
public:
virtual void fly();
};
class Penguin : public Bird {
public:
void fly() override {
throw std::logic_error("Can't fly");
}
};
This breaks substitution.
Correct Design
class Bird {};
class FlyingBird : public Bird {
public:
virtual void fly() = 0;
};
class Sparrow : public FlyingBird {};
class Penguin : public Bird {};
If you need to override a method to **disable it**, inheritance is wrong.
Interface Segregation Principle (ISP)
Clients should not be forced to depend on interfaces they do not use.
Bad Example
class Machine {
public:
virtual void print() = 0;
virtual void scan() = 0;
virtual void fax() = 0;
};
What if a device only prints?
Correct Design
class Printable {
public:
virtual void print() = 0;
};
class Scannable {
public:
virtual void scan() = 0;
};
Clients only depend on what they need.
ISP in Embedded Systems: Avoid “god drivers” with unused APIs.
Dependency Inversion Principle (DIP)
High-level modules should not depend on low-level modules, both should depend on abstractions.
Bad Example
class Motor {
public:
void start() {
// hardware-specific logic
}
};
class Car {
private:
Motor motor; // concrete dependency
public:
void drive() {
motor.start();
}
};
What’s wrong?
Caris a high-level conceptMotoris a low-level hardware detail- Changing the motor type requires changing
Car - Cannot test
Carwithout a realMotor
Correct Design
// Introduce an Abstraction
class IMotor {
public:
virtual void start() = 0;
virtual ~IMotor() = default;
};
// Make Low-Level Classes Depend on the Abstraction
class ElectricMotor : public IMotor {
public:
void start() override {
// electric motor logic
}
};
// High-Level Code Depends on the Abstraction (Interface)
class Car {
private:
IMotor* motor; // abstraction
public:
Car(IMotor* m) : motor(m) {}
void drive() {
motor->start();
}
};
// Testability
class MockMotor : public IMotor {
public:
void start() override {
// record call
}
};
Without DIP:
Car → Motor
With DIP:
Car → IMotor ← ElectricMotor
DIP and Dependency Injection (Important Difference)
These are not the same thing.
| DIP | Dependency Injection |
|---|---|
| Design principle | Implementation technique |
| Defines _what_ should depend on what | Defines _how_ dependencies are provided |
| Conceptual | Practical |
Example of dependency injection:
Car car(new ElectricMotor());
DIP Result
- Decoupling
- Testability
- Portability
Encapsulation
Hide internal state and expose behavior.
Bad Example
struct User {
int age;
};
Anyone can corrupt the state.
Correct design:
class User {
private:
int age;
public:
void setAge(int a);
};
Encapsulation protects invariants.
Abstract
Expose what an object does, not how it does it
class Sensor {
public:
virtual float read() = 0;
};
You don’t care how the value is read.
Composition Over Inheritance
Prefer composition over inheritance
Bad Example
class Logger {};
class FileLogger : public Logger {};
Inheritance creates tight coupling.
Good Design
class Logger {
public:
void log();
};
class App {
Logger logger;
};
Composition is flexible, inheritance is rigid.
Don't Repeat Yourself (DRY)
Bad
if (x > 0 && x < 100) {}
if (y > 0 && y < 100) {}
Good
bool isValid(int v) {
return v > 0 && v < 100;
}
You Aren't Gonna Need It (YAGNI)
Do not implement features until they are required
Over-engineering is technical debt in disguise.
Keep It Simple, Stupid (KISS)
Prefer simple solutions over clever ones.
Readable code beats clever code every time.
How These Principles Work Together
| Principle | Solves |
|---|---|
| SRP | Change isolation |
| OCP | Safe extension |
| LSP | Correct inheritance |
| ISP | Lean interfaces |
| DIP | Decoupling |
| Encapsulation | Safety |
| Abstraction | Simplicity |
| Composition | Flexibility |