Design Patterns in C++: When and Why
\n\nHave you ever rewritten code for the tenth time because new functionality broke the old architecture? Or spent hours debugging, trying to figure out who changed a global variable and why? Sound familiar? Then welcome to the world of design patterns.
\n\nPatterns are not a magic pill. They are battle-tested solutions to common problems. They won't make your code perfect, but they will help you speak the same language as other developers and avoid the pitfalls that thousands of programmers have already stepped into before you.
\n\nIn this article, we will break down three main groups of patterns in C++: creational, structural, and behavioral. We'll learn when to apply them and when it's better to pass them by. Let's go!
\n\nCreational Patterns: Control Over Object Creation
\n\nCreational patterns are responsible for flexible object creation. They hide the instantiation logic and make the system independent of exactly how its components are created.
\n\nSingleton
\n\nA classic. It guarantees that a class has only one instance and provides a global point of access to it. Sounds tempting, but use it wisely — Singleton often becomes an anti-pattern if it bloats into a "god object."
\n\nWhen to use: logger, configuration manager, thread pool, working with hardware.
\n\nExample in C++ (thread-safe, C++11 and newer):
\n\nclass Logger {\npublic:\n static Logger& getInstance() {\n static Logger instance; // Initialized once, even in multithreading\n return instance;\n }\n\n void log(const std::string& msg) {\n std::cout << "[LOG]: " << msg << std::endl;\n }\n\n // Disable copying and assignment\n Logger(const Logger&) = delete;\n Logger& operator=(const Logger&) = delete;\n\nprivate:\n Logger() = default; // Private constructor\n ~Logger() = default;\n};\n\n// Usage\nLogger::getInstance().log("System started");\n\nFactory Method
\n\nDefines an interface for creating an object but lets subclasses alter the type of object that will be created. It's great when you don't know in advance exactly which class will be needed.
\n\nWhen to use: parsing different formats (JSON, XML, YAML), creating UI elements for different OSes, working with different databases.
\n\nExample:
\n\n// Abstract product\nclass Document {\npublic:\n virtual void open() = 0;\n virtual ~Document() = default;\n};\n\n// Concrete products\nclass PDFDocument : public Document {\n void open() override { std::cout << "Opening PDF" << std::endl; }\n};\n\nclass WordDocument : public Document {\n void open() override { std::cout << "Opening Word" << std::endl; }\n};\n\n// Factory\nclass Application {\npublic:\n virtual Document* createDocument() = 0; // Factory method\n virtual ~Application() = default;\n\n void newDocument() {\n Document* doc = createDocument();\n doc->open();\n delete doc; // In real code — smart pointers\n }\n};\n\nclass PDFApp : public Application {\n Document* createDocument() override { return new PDFDocument(); }\n};\n\n// Usage\nPDFApp app;\napp.newDocument(); // Opening PDF\n\nStructural Patterns: Building the Constructor
\n\nThese patterns help organize classes and objects into larger structures while maintaining flexibility and efficiency.
\n\nAdapter
\n\nAllows objects with incompatible interfaces to work together. Imagine you have an old library for working with XML, but a new module expects JSON. The Adapter is your savior.
\n\nWhen to use: