SE: Software Design Principles

Good software is not defined by syntax, language, or framework.It is defined by how it reacts to change.

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)

cpp
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)

cpp
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

cpp
void processPayment(int type) {
    if (type == 1) payByCard();
    else if (type == 2) payByPaypal();
}

Adding a new payment method means editing existing code.

Correct Design

cpp
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

cpp
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

cpp
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

cpp
class Machine {
public:
    virtual void print() = 0;
    virtual void scan() = 0;
    virtual void fax() = 0;
};

What if a device only prints?

Correct Design

cpp
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

cpp
class Motor {
public:
    void start() {
        // hardware-specific logic
    }
};

class Car {
private:
    Motor motor;   // concrete dependency
public:
    void drive() {
        motor.start();
    }
};

What’s wrong?

  • Car is a high-level concept
  • Motor is a low-level hardware detail
  • Changing the motor type requires changing Car
  • Cannot test Car without a real Motor

Correct Design

cpp
// 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.

DIPDependency Injection
Design principleImplementation technique
Defines _what_ should depend on whatDefines _how_ dependencies are provided
ConceptualPractical

Example of dependency injection:

cpp
Car car(new ElectricMotor());

DIP Result

  • Decoupling
  • Testability
  • Portability

Encapsulation

Hide internal state and expose behavior.

Bad Example

cpp
struct User {
    int age;
};

Anyone can corrupt the state.

Correct design:

cpp
class User {
private:
    int age;
public:
    void setAge(int a);
};

Encapsulation protects invariants.


Abstract

Expose what an object does, not how it does it

cpp
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

cpp
class Logger {};
class FileLogger : public Logger {};

Inheritance creates tight coupling.

Good Design

cpp
class Logger {
public:
    void log();
};

class App {
    Logger logger;
};

Composition is flexible, inheritance is rigid.


Don't Repeat Yourself (DRY)

Bad

cpp
if (x > 0 && x < 100) {}
if (y > 0 && y < 100) {}

Good

cpp
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

PrincipleSolves
SRPChange isolation
OCPSafe extension
LSPCorrect inheritance
ISPLean interfaces
DIPDecoupling
EncapsulationSafety
AbstractionSimplicity
CompositionFlexibility