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