Skip to main content

SOLID

Single responsibility principal

  • A class must not have more than one responsibility.
  • Higher Cohesion: The code inside the class actually belongs together.
  • Lower Coupling: Classes depend on each other less, making it safer to swap parts out.
  • Testability: It’s much easier to write a unit test for a class that only does one thing.
  • The God Object is the anti-pattern that SRP solves
class Policy
{
void apply();
void logoutUser(); // This is bad
}

Open/Close Principal

  • Open for extension, closed for modification
  • A class should not be modified once it has been written.
  • Regression Bugs: Breaking a feature that was working perfectly for months.
  • Testability: You don't have to retest everything since it is closed for modification.
  • Merge Conflicts: Multiple developers trying to edit the same core file for different features.
// Closed for modification
class IPolicy
{
virtual ~IPolicy() = default;
virtual void apply(const User &user) = 0;
}

// New features are added here, open for extension
class RemoteWorkPolicy : public IPolicy {
void apply(const User &user) override {
// Business logic...
}
};

class Attandance
{
// Open for extension through dependency injection
IPolicy* m_Policy;

Attendance(IPolicy* policy) : m_Policy(policy) {}

void markAttandance(const User &user)
{
if(!policy) throw std::runtime_exception("Unexpected");

policy.apply(user);
}
}

Liskov Substitution Principle

  • If (S) is a subtype of (T), then objects of type (T) should be replaceable with objects of type (S) without breaking the application.
  • The Square-Rectangle problem: If a Square inherits from Rectangle but throws an error when you try to set the width and height to different values, it violates LSP. Code expecting a Rectangle might behave incorrectly when passed a Square.
  • Principle of Least Astonishment: If a function takes an IPolicy*, it should be able to use any subclass without needing to check which one it is or handling unexpected "Not Implemented" errors.

Violation of LSP

class IPolicy {
public:
virtual ~IPolicy() = default;
virtual void apply(const User& user) = 0;
};

// It only works for Admin users, otherwise it crashes/throws.
class AdminOnlyPolicy : public IPolicy {
public:
void apply(const User& user) override {
if (!user.isAdmin()) {
// This violates LSP because the caller of IPolicy
// doesn't expect apply() to fail based on user type.
throw std::runtime_error("LSP Broken: Unexpected user type!");
}

// Business logic...
}
};

The base contract:

virtual void apply(const User& user) = 0;

implies:

  • Any User is valid input.
  • Callers should be able to pass any User to any IPolicy.

But AdminOnlyPolicy secretly strengthens the precondition:

if (!user.isAdmin())

Now the function only accepts a subset of User.

That means code like this becomes unsafe:

void runPolicy(IPolicy& policy, const User& user) {
policy.apply(user);
}

Solution to the violation

One possible solution to this problem

class IPolicy {
public:
virtual ~IPolicy() = default;

// We define a new method, it returns false if it's not applicable
virtual bool canApply(const User& user) const = 0;
virtual void apply(const User& user) = 0;
};

class AdminOnlyPolicy : public IPolicy {
public:
bool canApply(const User& user) const override {
return !user.isAdmin(); // Explicitly states its limits
}

void apply(const User& user) override {
// We know this is safe because the caller checks canApply first
user.setPermissions(LOW);
}
};

// The consumer can now use any IPolicy safely
void processAttendance(User& user, IPolicy* policy) {
if (policy->canApply(user))
policy->apply(user); // No surprises here
}

Make rejection part of the base contract:

virtual bool canApply(const User& user) const = 0;

Then callers know rejection is normal behavior rather than a broken substitution.

Interface Segregation Principle

  • No client should be forced to depend on methods it does not use.
  • Splitting the "Fat Interface": Instead of one massive IPolicy that handles attendance, payroll, and security, you split them into smaller, focused interfaces.
  • This prevents "Side-Effect Recompilation." If you change a payroll method in a giant interface, your attendance module (which doesn't care about payroll) still has to recompile.
// A "Fat" Interface
class IEmployeeActions {
virtual void calculatePay() = 0; // for Accountant
virtual void writeCode() = 0; // for Developer - Why does the Accountant need this?
};

// Segregated Interfaces
class IDeveloper {
virtual void writeCode() = 0;
};

class IAccountant {
virtual void calculatePay() = 0;
};

Dependency Inversion Principle

  • High-level modules should not depend on low-level modules. Both should depend on abstractions.
  • Attendance class (high-level) shouldn't know about WiFiPolicy or FingerprintPolicy (low-level details). It only knows about IPolicy (the abstraction).
  • This is the ultimate goal of the previous four principles. It allows you to swap out a database, a UI framework, or a business rule without touching the core orchestration logic.

Attendance depends on the abstraction IPolicy. It does not depend on any concrete class

class IPolicy
{
virtual ~IPolicy() = default;
virtual void apply(const User &user) = 0;
}

class Attandance
{
IPolicy* m_Policy;

Attendance(IPolicy* policy) : m_Policy(policy) {}
}