Interfaces

Abstract contracts in C++

🔌 What are Interfaces?

Interfaces in C++ are abstract classes with only pure virtual functions. They define contracts that implementing classes must follow, ensuring consistent behavior across different implementations without providing implementation details.


// Interface example
class IDrawable {
public:
    virtual void draw() = 0;
    virtual ~IDrawable() = default;
};
                                    

Key Interface Concepts

📋

Contract

Defines what methods must be implemented

virtual void method() = 0;
🔄

Multiple Inheritance

Classes can implement multiple interfaces

class A : public I1, public I2
🎯

Polymorphism

Different implementations of same interface

IShape* shape = new Circle();
🔧

Decoupling

Reduces dependencies between components

void process(IProcessor* p)

🔹 Creating and Using Interfaces

Interfaces in C++ are typically implemented using abstract classes with pure virtual functions, defining a contract for derived classes. By declaring methods like virtual void draw() = 0 and virtual double area() = 0, you enforce implementation in any concrete class such as Circle. This abstraction allows you to write flexible, modular code that works with any shape, enabling polymorphism. For instance, you can call draw() on a pointer to the interface to render circles at various coordinates, calculating areas consistently across all shape types.

#include <iostream>
#include <vector>
#include <memory>
using namespace std;

// Interface for drawable objects
class IDrawable {
public:
    virtual void draw() const = 0;
    virtual double getArea() const = 0;
    virtual ~IDrawable() = default;
};

// Interface for movable objects
class IMovable {
public:
    virtual void move(int x, int y) = 0;
    virtual ~IMovable() = default;
};

// Circle implements both interfaces
class Circle : public IDrawable, public IMovable {
private:
    double radius;
    int posX, posY;
    
public:
    Circle(double r) : radius(r), posX(0), posY(0) {}
    
    void draw() const override {
        cout << "Drawing circle at (" << posX << "," << posY 
             << ") with radius " << radius << endl;
    }
    
    double getArea() const override {
        return 3.14159 * radius * radius;
    }
    
    void move(int x, int y) override {
        posX = x;
        posY = y;
    }
};

int main() {
    Circle circle(5.0);
    
    // Use as IDrawable
    IDrawable* drawable = &circle
    drawable->draw();
    cout << "Area: " << drawable->getArea() << endl;
    
    // Use as IMovable
    IMovable* movable = &circle
    movable->move(10, 20);
    drawable->draw(); // Shows new position
    
    return 0;
}

Output:

Drawing circle at (0,0) with radius 5

Area: 78.5397

Drawing circle at (10,20) with radius 5

🔹 Interface Segregation Principle

The Interface Segregation Principle (ISP) advises creating small, client-specific interfaces instead of large, general-purpose ones. This means splitting a broad interface into more focused ones so that implementing classes are not forced to depend on methods they do not use. For example, instead of a single Printer interface with print(), scan(), and fax() methods, you could define separate Printable and Scannable interfaces. This improves code maintainability, reduces side effects from changes, and adheres to clean architecture principles.

// Bad: Fat interface
class IBadPrinter {
public:
    virtual void print() = 0;
    virtual void scan() = 0;
    virtual void fax() = 0;
    virtual void email() = 0;
};

// Good: Segregated interfaces
class IPrinter {
public:
    virtual void print() = 0;
    virtual ~IPrinter() = default;
};

class IScanner {
public:
    virtual void scan() = 0;
    virtual ~IScanner() = default;
};

class IFax {
public:
    virtual void fax() = 0;
    virtual ~IFax() = default;
};

// Simple printer only implements what it needs
class SimplePrinter : public IPrinter {
public:
    void print() override {
        cout << "Printing document..." << endl;
    }
};

// Multi-function printer implements multiple interfaces
class MultiFunctionPrinter : public IPrinter, public IScanner, public IFax {
public:
    void print() override { cout << "Printing..." << endl; }
    void scan() override { cout << "Scanning..." << endl; }
    void fax() override { cout << "Faxing..." << endl; }
};

🔹 Dependency Injection with Interfaces

Dependency Injection (DI) leverages interfaces to decouple classes and enhance testability and flexibility. Instead of hardcoding dependencies, you inject them via constructors or setters. For example, a UserService could depend on an abstract Logger interface, allowing you to inject a ConsoleLogger or a FileLogger at runtime. This makes unit testing easier, as you can inject mock loggers, and promotes the Open/Closed Principle by enabling new logger implementations without modifying existing client code, such as logging user creation events.

// Logger interface
class ILogger {
public:
    virtual void log(const string& message) = 0;
    virtual ~ILogger() = default;
};

// Different logger implementations
class ConsoleLogger : public ILogger {
public:
    void log(const string& message) override {
        cout << "[CONSOLE] " << message << endl;
    }
};

class FileLogger : public ILogger {
public:
    void log(const string& message) override {
        cout << "[FILE] Writing to file: " << message << endl;
    }
};

// Service that depends on logger interface
class UserService {
private:
    ILogger* logger;
    
public:
    UserService(ILogger* log) : logger(log) {}
    
    void createUser(const string& username) {
        // Business logic here
        logger->log("User created: " + username);
    }
};

int main() {
    ConsoleLogger consoleLog;
    FileLogger fileLog;
    
    UserService service1(&consoleLog);
    service1.createUser("Alice");
    
    UserService service2(&fileLog);
    service2.createUser("Bob");
    
    return 0;
}

Output:

[CONSOLE] User created: Alice

[FILE] Writing to file: User created: Bob

🧠 Test Your Knowledge

What makes a class an interface in C++?