CLOSE
Updated on 22 Jun, 202623 mins read 21 views

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 support

Three months later:

We want Razorpay support

Later:

We want UPI support

Now the developer starts modifying:

OrderService

again and again.

Every new payment provider requires:

Modification
Retesting
Redeployment

The system becomes increasingly rigid.

The root problem is:

OrderService depends on a concrete implementation.

Instead of depending on:

A payment capability

This 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 capability

For example:

Process Payment

remains the same.

Only the implementation changes.

Therefore:

A good design should depend on:

Capabilities

rather than:

Specific implementations

The Core Idea

The principle can be summarized as:

Depend on abstractions, not concrete classes.

Instead of saying:

Use Paypal

say:

Use something that can process payments.

Instead of saying:

Use MySQL

say:

Use something that can store data.

Instead of saying:

Use Gmail

say:

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 Charger

The phone cares about:

USB-C Interface

As long as the charger satisfies the contract:

Provide Power Through USB-C

the phone works.

The phone programs to:

Interface

not:

Implementation

This is exactly what we want in software.

What Is an Interface?

Conceptually:

An interface defines:

What can be done

not

How it is done

Example:

class IPaymentProcessor
{
public:

    virtual void pay(double amount) = 0;

    virtual ~IPaymentProcessor() = default;
};

This defines:

A Payment Capability

It does NOT define:

PayPal
Stripe
Razorpay

Those 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
PayPalPayment

Problems:

  • 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    Stripe

OrderService knows:

Capability

not:

Implementation

This is the essence of the principle.

Why This is Powerful

Suppose tomorrow we add:

class RazorPayment
	: public IPaymentProcessor
{
};

OrderService changes?

No

Architecture changes?

No

Only a new implementation appears.

This is extensibility.

Understanding Dependency Direction

Bad design:

Business Logic
      |
      v
Concrete Technology

Good design:

Business Logic
      |
      v
Abstraction
      ^
      |
Concrete Technology

Notice:

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:

MySQLRepository
PostgreSQLRepository

UserService remains unchanged.

Logging Example

Bad:

class ReportGenerator
{
private:
	FileLogger logger;
};

Later:

Need Cloud Logging

Refactor required.

Better:

class ILogger
{
public:
	virtual void log() = 0;
};

Implementations:

FileLogger
CloudLogger
ConsoleLogger

ReportGenerator 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: IPaymentProcessor

Programming 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:

StripePayment

extends 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:

OrderService

requires:

PayPalPayment

Testing 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.

 

Buy Me A Coffee

Leave a comment

Your email address will not be published. Required fields are marked *

Your experience on this site will be improved by allowing cookies Cookie Policy