Imagine you are building an e-commerce platform.
You need to process payments.
A beginner writes:
class PayPalPayment
{
public:
void pay(double amount)
{
std::cout << "Processing PayPal Payment\n";
}
};
class OrderService
{
private:
PayPalPayment paymentProcessor;
public:
void checkout(double amount)
{
paymentProcessor.pay(amount);
}
};Everything works.
Six months later, the business says:
We want Stripe supportThree months later:
We want Razorpay supportLater:
We want UPI supportNow the developer starts modifying:
OrderServiceagain and again.
Every new payment provider requires:
Modification
Retesting
RedeploymentThe system becomes increasingly rigid.
The root problem is:
OrderService depends on a concrete implementation.Instead of depending on:
A payment capabilityThis problem led to one of the most important principles in software design:
Program to Interfaces, Not Implementations.
Why This Principle Exists
Let's understand the fundamental problem.
Every software system changes.
What changes?
Database change.
Payment providers change.
Authentication providers change.
Messaging systems change.
Storage systems change.
External APIs change.What usually does NOT change?
The business capabilityFor example:
Process Paymentremains the same.
Only the implementation changes.
Therefore:
A good design should depend on:
Capabilitiesrather than:
Specific implementationsThe Core Idea
The principle can be summarized as:
Depend on abstractions, not concrete classes.
Instead of saying:
Use Paypalsay:
Use something that can process payments.Instead of saying:
Use MySQLsay:
Use something that can store data.Instead of saying:
Use Gmailsay:
Use something that can send emails.This subtle shift changes the entire architecture.
Real-World Analogy
Imagine you buy a phone charger.
The phone doesn't care:
Samsung Charger
Apple Charger
Nokia ChargerThe phone cares about:
USB-C InterfaceAs long as the charger satisfies the contract:
Provide Power Through USB-Cthe phone works.
The phone programs to:
Interfacenot:
ImplementationThis is exactly what we want in software.
What Is an Interface?
Conceptually:
An interface defines:
What can be donenot
How it is doneExample:
class IPaymentProcessor
{
public:
virtual void pay(double amount) = 0;
virtual ~IPaymentProcessor() = default;
};This defines:
A Payment CapabilityIt does NOT define:
PayPal
Stripe
RazorpayThose are implementations.
The Wrong Design
Let's start the problematic design:
class PayPalPayment
{
public:
void pay(double amount)
{
std::cout << "PayPal Payment\n";
}
};class OrderService
{
private:
PayPalPayment payment;
public:
void checkout(double amount)
{
payment.pay(amount);
}
};Dependency diagram:
OrderService
|
v
PayPalPaymentProblems:
- Tight Coupling
- OrderService knows PayPal
- Difficult Testing
- Need real PayPal implementation
- Hard To Extend
- New provider requires modifications
- Violates OCP
- Must modify existing code.
The Better Design
Create an abstraction.
class IPaymentProcessor
{
public:
virtual void pay(double amount) = 0;
virtual ~IPaymentProcessor() = default;
};Implementations:
class PayPalPayment
: IPaymentProcessor
{
public:
void pay(double amount) override
{
std::cout << "Stripe\n";
}
};OrderService:
class OrderService
{
private:
IPaymentProcessor& payment;
public:
OrderService(IPaymentProcessor& p)
: payment(p)
{
}
void checkout(double amount)
{
payment.pay(amount);
}
};Now:
OrderService
|
v
IPaymentProcessor
|
|
+-----------+
| |
v v
PayPal StripeOrderService knows:
Capabilitynot:
ImplementationThis is the essence of the principle.
Why This is Powerful
Suppose tomorrow we add:
class RazorPayment
: public IPaymentProcessor
{
};OrderService changes?
NoArchitecture changes?
NoOnly a new implementation appears.
This is extensibility.
Understanding Dependency Direction
Bad design:
Business Logic
|
v
Concrete TechnologyGood design:
Business Logic
|
v
Abstraction
^
|
Concrete TechnologyNotice:
Business logic no longer depends on technology.
Technology depends on business contracts.
This is major architectural shift.
Database Example
Bad:
class UserService
{
private:
MySQLDatabase database;
};What happens when company adopts:
PostgreSQL?Changes required.
Better:
class IUserRepository
{
public:
virtual void saveUser() = 0;
};Implementations:
MySQLRepositoryPostgreSQLRepositoryUserService remains unchanged.
Logging Example
Bad:
class ReportGenerator
{
private:
FileLogger logger;
};Later:
Need Cloud LoggingRefactor required.
Better:
class ILogger
{
public:
virtual void log() = 0;
};Implementations:
FileLogger
CloudLogger
ConsoleLoggerReportGenerator stays stable.
The Relationship With DIP
Recall DIP:
High-level modules should not depend on low-level modules.
How do we achieve that?
By programming to interfaces.
Example:
OrderService
should not depend on: Paypal
It should depend on: IPaymentProcessorProgramming to interfaces is one of the primary mechanisms that enables DIP.
The Relationship With OCP
Recall OCP:
Open for extension, closed for modification.
Programming to interfaces makes this possible.
Why?
Because adding:
StripePaymentextends the system.
No modifications required.
Without interfaces:
OCP becomes difficult.
The Relationship With Composition
Remember previous chapter:
Favor Composition Over Inheritance.
Composition often relies on interfaces.
Example:
class OrderService
{
private:
IPaymentProcessor& paymentProcessor;
};The behavior becomes pluggable.
This is composition powered by abstraction.
Dependency Injection Emerges Naturally
Consider:
OrderService(IPaymentProcessor& payment)
Where does payment come from?
Someone provides it.
This is Dependency Injection
Example:
PayPalPayment paypal;
OrderService order(paypal);Dependencies are supplied externally.
Not created internally.
This is another major architectural concept.
Testing benefits
One of the biggest reasons professional love interfaces.
Without interfaces:
OrderServicerequires:
PayPalPaymentTesting becomes difficult.
With interfaces:
Create a fake.
class MockPaymentProcessor
: public IPaymentProcessor
{
pubic:
bool called = false;
void pay(double amount) override
{
called = true;
}
};Test:
MockPaymentProcessor mock;
OrderService service(mock);
service.checkout(100);No real payment provider required.
Testing becomes trivial.
Leave a comment
Your email address will not be published. Required fields are marked *
