Skip to Content
Course content

34: Interfaces via Abstract Base Classes

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I've been thinking about how we handle logging in our project. Right now, we're just using std::cout everywhere, which is fine for a prototype, but it's a nightmare for production. We need a way to swap where our logs go—maybe a file, maybe a database, or maybe a cloud service—without changing every single line of code that calls log().

The problem with generic loggers

My first instinct was to just make a base class. Something simple. Let's see what happens when I try to set this up the "obvious" way.

class Logger {
public:
    void log(const std::string& message) {
        // What goes here? I don't actually know how to log 
        // until I know WHERE I'm logging to.
    }
};

class ConsoleLogger : public Logger {
public:
    void log(const std::string& message) override {
        std::cout << "[Console]: " << message << std::endl;
    }
};

Wait, I just noticed a problem. If I write this, the compiler lets me do this: Logger myLogger; myLogger.log("Hello");. But a generic Logger doesn't actually do anything. It's a useless object. It's a conceptual "idea" of a logger, not a functional tool. In a large codebase, someone will eventually instantiate the base class by mistake, and your logs will just vanish into a void because the base log() method is empty.

Stopping the "Empty" Implementation

I don't want to provide a default implementation because there is no such thing as a "default" way to log. I want to force whoever creates a new logger to define exactly how log() works. Let's try making it a pure virtual function. I'll add = 0 to the end of the declaration.

class Logger {
public:
    virtual ~Logger() {} // Always remember the virtual destructor for base classes!
    virtual void log(const std::string& message) = 0; 
};

Now, let's see what happens if I try to create a generic logger again: Logger myLogger;. The compiler immediately throws a fit. It tells me that Logger is an "abstract class" and cannot be instantiated. This is exactly what I wanted. The compiler is now acting as my quality control, ensuring that no one can ever create a "half-baked" logger.

Letting the Compiler Enforce the Contract

Now, here is where it gets interesting. If I create a FileLogger but I forget to implement the log() method, I'd expect it to work just like the base class. Let's test that theory.

class FileLogger : public Logger {
    // I'm "forgetting" to implement log() here
};

The compiler catches me again. It refuses to build FileLogger because it's still abstract. By using a pure virtual function, I've created an Interface. I'm not telling the derived classes how to log; I'm telling them what they must be able to do if they want to be considered a Logger.

Tying it all together with a Pointer

The real payoff happens when we use these interfaces in our actual logic. I don't want my UserAuth class to know if it's talking to a console or a file; it just needs to know that it has something that can log.

void processLogin(Logger* logger) {
    // This function doesn't care about the specific implementation
    logger->log("User attempted login...");
}

int main() {
    ConsoleLogger cLog;
    FileLogger fLog;

    processLogin(&cLog); // Works!
    processLogin(&fLog); // Also works!
}

This is the essence of an interface via an abstract base class. We've decoupled the usage of the logger from the implementation of the logger. If we decide next week to send logs to a Slack channel, we just create a SlackLogger that implements the interface, and processLogin doesn't have to change a single character of code. It's clean, it's safe, and the compiler does the heavy lifting of making sure we didn't forget any methods.




📋 Practical Task

Build a Multi-Provider Payment System

You are building a checkout system that needs to support multiple payment methods (e.g., Credit Card, PayPal, Bitcoin). You don't want the checkout logic to be rewritten every time a new payment provider is added.

Your Task:

  • Create an abstract base class named PaymentProcessor.
  • Define a pure virtual function processPayment(double amount) that returns a bool.
  • Implement two derived classes: CreditCardProcessor and PayPalProcessor. Each should print a unique message (e.g., "Processing credit card payment of $X") and return true.
  • Write a function called executeTransaction(PaymentProcessor* processor, double amount) that takes a pointer to the interface and calls the processPayment method.
  • In your main() function, instantiate both processors and pass them into executeTransaction to demonstrate that the same function can handle different payment types.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.