-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Object-Oriented C++
-
Section 4: Memory Management
-
Section 5: Templates and Generic Programming
-
Section 6: The Standard Template Library
-
Section 7: Modern C++ Features
-
Section 8: Error Handling
-
Section 9: Multithreading
-
Section 10: Operator Overloading and Type Conversion
-
Section 11: Advanced Topics
-
Section 12: Tooling and Build Systems
-
Section 13: Design Patterns in C++
-
Section 14: Interfacing with C and Systems Programming
-
Section 15: Networking and IPC Basics
-
Section 16: Graphics and Game Programming Basics
-
Section 17: The Boost Libraries
-
Section 18: Data Structures and Algorithms in C++
-
Section 19: Practical Projects
-
Section 20: More Concurrency Patterns
-
Section 21: More OOP and Design Practice
-
Section 22: File I/O and Streams
-
Section 23: More Standard Library
-
Section 24: Practice Exercises
-
Section 25: Interview and Algorithm Practice
-
Section 26: Compiler and Language Internals
-
Section 27: GUI and Application Frameworks Overview
-
Section 28: Testing and Quality Practices
-
Section 29: Numerics Library
-
Section 30: Concepts Library (C++20)
-
Section 31: Ranges Library (C++20) In Depth
-
Section 32: More Utility Library
193: Practice Exercise: Building a Simple Plugin Architecture with Abstract Interfaces
A few years ago, I worked with a developer named Marcus who was building a data ingestion tool. Every time a client wanted to support a new file format—say, moving from CSV to Parquet or Avro—Marcus had to open the core processing loop and add another else if block. By the time they supported ten formats, that single function was a three-thousand-line nightmare. The worst part? Every time he added a new format, he risked breaking the existing ones because he was touching the "golden" core logic. He was terrified to compile the project.
This is exactly why we use abstract interfaces for plugin architectures. Instead of the core application knowing every possible format, the application defines a "contract" that any plugin must follow. The core doesn't care how a file is parsed; it only cares that the object it's holding has a parse() method. This transforms your code from a rigid monolith into a flexible system where you can add functionality without ever touching the existing, tested logic.
Defining the Interface Contract
To get this right, you need a base class that consists entirely of pure virtual functions. I always tell people to prefix these with an 'I' (like IPlugin or IDataParser)—it's a convention that immediately tells anyone reading your code, "You cannot instantiate this; it's a blueprint."
class IDataParser {
public:
virtual ~IDataParser() = default; // Never forget the virtual destructor in interfaces!
virtual bool canHandle(const std::string& extension) = 0;
virtual std::string parse(const std::string& filePath) = 0;
};
That virtual destructor is non-negotiable. Since the main application will be deleting these plugins via a pointer to the interface, failing to make the destructor virtual leads to undefined behavior and memory leaks. It's one of those C++ "gotchas" that can haunt you for weeks if you forget it.
Decoupling Execution from Implementation
Once the interface is set, your main application logic should operate on a collection of these interfaces, not the concrete classes. I prefer using std::unique_ptr here to ensure ownership is clear. The main engine simply iterates through its list of registered plugins and asks, "Can you handle this file?"
std::vector<std::unique_ptr<IDataParser>> plugins;
// Adding plugins (this could happen via a config file or a registry)
plugins.push_back(std::make_unique<CsvParser>());
plugins.push_back(std::make_unique<JsonParser>());
// The core logic remains blissfully ignorant of the concrete types
for (const auto& plugin : plugins) {
if (plugin->canHandle(".json")) {
std::string data = plugin->parse("config.json");
// process data...
}
}
Notice how the loop doesn't change regardless of whether you have two plugins or two hundred. You've successfully isolated the "what" (parsing data) from the "how" (the specific logic for JSON or CSV).
Managing Plugin Lifetimes and Registration
In a real-world production system, you wouldn't manually push_back every plugin in main(). You'd likely have a PluginManager class. I've found that a simple Factory pattern works best here. The manager maintains a map of creators or simply loads shared libraries (.dll or .so files) at runtime using dlopen or LoadLibrary.
Even in a simpler, compile-time plugin system, keeping the registration logic separate from the execution logic is key. It allows you to swap out implementations—perhaps replacing a slow, legacy parser with a high-performance one—without the rest of the system even noticing the change. You're essentially treating your own code as a set of interchangeable parts.
📋 Practical Task
Exercise: Building a Multi-Format Log Analyzer Plugin System
Your goal is to implement a plugin-based log analysis system. The system should be able to process different log formats (e.g., Simple Text and JSON) without the main engine knowing the details of those formats.
- The Interface: Create an abstract base class
ILogParserwith a virtual destructor and two pure virtual methods:bool isValid(const std::string& line)andstd::string extractMessage(const std::string& line). - The Concrete Plugins:
- Implement
TextLogParser: It considers a line valid if it starts with "[INFO]" or "[ERROR]" and extracts everything after the prefix. - Implement
JsonLogParser: It considers a line valid if it contains the characters{"msg":and extracts the value associated with the "msg" key.
- Implement
- The Engine: Create a
LogProcessorclass that holds astd::vectorofstd::unique_ptr<ILogParser>. Implement a methodprocessLine(const std::string& line)that iterates through all loaded plugins and prints the extracted message if a plugin identifies the line as valid. - The Test: In your
mainfunction, register both parsers into theLogProcessorand pass it a list of mixed strings (some JSON-like, some text-like) to verify that the correct plugin handles the correct line.
There are no comments for now.