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
Useris valid input. - Callers should be able to pass any
Userto anyIPolicy.
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
IPolicythat handlesattendance,payroll, andsecurity, 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.
Attendanceclass (high-level) shouldn't know aboutWiFiPolicyorFingerprintPolicy(low-level details). It only knows aboutIPolicy(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) {}
}