Паттерны проектирования в C++: когда и зачем

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Паттерны проектирования в C++: когда и зачем

Вы когда-нибудь переписывали код по десятому кругу, потому что новый функционал ломал старую архитектуру? Или тратили часы на дебаг, пытаясь понять, кто и зачем изменил глобальную переменную? Знакомо? Тогда добро пожаловать в мир паттернов проектирования.

Паттерны — это не волшебная таблетка. Это проверенные бойцами решения типовых проблем. Они не сделают ваш код идеальным, но помогут говорить на одном языке с другими разработчиками и избежать граблей, на которые уже наступили тысячи программистов до вас.

В этой статье мы разберём три основные группы паттернов в C++: порождающие, структурные и поведенческие. Узнаем, когда их стоит применять, а когда лучше пройти мимо. Поехали!

Порождающие паттерны: контроль над созданием объектов

Порождающие паттерны отвечают за гибкое создание объектов. Они скрывают логику инстанцирования и делают систему независимой от того, как именно создаются её компоненты.

Singleton (Одиночка)

Классика. Гарантирует, что у класса есть только один экземпляр, и предоставляет глобальную точку доступа к нему. Звучит заманчиво, но используйте его с умом — Singleton часто становится антипаттерном, если раздувается до размеров «бога-объекта».

Когда использовать: логгер, менеджер конфигурации, пул потоков (thread pool), работа с аппаратным обеспечением.

Пример в C++ (thread-safe, C++11 и новее):

class Logger {
public:
    static Logger& getInstance() {
        static Logger instance; // Инициализируется один раз, даже в многопоточке
        return instance;
    }

    void log(const std::string& msg) {
        std::cout << "[LOG]: " << msg << std::endl;
    }

    // Запрещаем копирование и присваивание
    Logger(const Logger&) = delete;
    Logger& operator=(const Logger&) = delete;

private:
    Logger() = default; // Приватный конструктор
    ~Logger() = default;
};

// Использование
Logger::getInstance().log("Система запущена");

Factory Method (Фабричный метод)

Определяет интерфейс для создания объекта, но позволяет подклассам изменять тип создаваемого объекта. Отлично подходит, когда вы заранее не знаете, какой именно класс понадобится.

Когда использовать: парсинг разных форматов (JSON, XML, YAML), создание UI-элементов под разные ОС, работа с разными базами данных.

Пример:

// Абстрактный продукт
class Document {
public:
    virtual void open() = 0;
    virtual ~Document() = default;
};

// Конкретные продукты
class PDFDocument : public Document {
    void open() override { std::cout << "Открываем PDF" << std::endl; }
};

class WordDocument : public Document {
    void open() override { std::cout << "Открываем Word" << std::endl; }
};

// Фабрика
class Application {
public:
    virtual Document* createDocument() = 0; // Фабричный метод
    virtual ~Application() = default;

    void newDocument() {
        Document* doc = createDocument();
        doc->open();
        delete doc; // В реальном коде — умные указатели
    }
};

class PDFApp : public Application {
    Document* createDocument() override { return new PDFDocument(); }
};

// Использование
PDFApp app;
app.newDocument(); // Открываем PDF

Структурные паттерны: собираем конструктор

Эти паттерны помогают организовать классы и объекты в более крупные структуры, сохраняя гибкость и эффективность.

Adapter (Адаптер)

Позволяет объектам с несовместимыми интерфейсами работать вместе. Представьте, что у вас есть старая библиотека для работы с XML, а новый модуль ждёт JSON. Адаптер — ваш спаситель.

Когда использовать: интеграция легаси-кода, подключение сторонних библиотек с неудобным интерфейсом.

Пример:

// Старый интерфейс
class OldXMLParser {
public:
    std::string parseXML(const std::string& data) {
        return "XML: " + data;
    }
};

// Новый интерфейс, который мы хотим использовать
class NewJSONParser {
public:
    virtual std::string parseJSON(const std::string& data) = 0;
    virtual ~NewJSONParser() = default;
};

// Адаптер
class XMLtoJSONAdapter : public NewJSONParser {
private:
    OldXMLParser* adaptee;
public:
    XMLtoJSONAdapter(OldXMLParser* parser) : adaptee(parser) {}

    std::string parseJSON(const std::string& data) override {
        // Переводим JSON в XML (упрощённо) и вызываем старый парсер
        std::string xmlData = "<data>" + data + "</data>";
        return adaptee->parseXML(xmlData);
    }
};

// Использование
OldXMLParser legacyParser;
XMLtoJSONAdapter adapter(&legacyParser);
std::cout << adapter.parseJSON("{key:value}"); // XML: <data>{key:value}</data>

Composite (Компоновщик)

Позволяет работать с отдельными объектами и их группами одинаково. Дерево файловой системы, графические сцены, иерархия меню — классические примеры.

Когда использовать: любая древовидная структура, где нужно единообразно обрабатывать листья и узлы.

#include <vector>
#include <memory>

class Graphic {
public:
    virtual void draw() const = 0;
    virtual ~Graphic() = default;
};

class Circle : public Graphic {
public:
    void draw() const override { std::cout << "Рисуем круг\n"; }
};

class CompoundGraphic : public Graphic {
private:
    std::vector<std::shared_ptr<Graphic>> children;
public:
    void add(std::shared_ptr<Graphic> child) { children.push_back(child); }
    void draw() const override {
        for (const auto& child : children)
            child->draw();
    }
};

// Использование
CompoundGraphic group;
group.add(std::make_shared<Circle>());
group.add(std::make_shared<Circle>());
group.draw(); // Рисуем круг дважды

Поведенческие паттерны: налаживаем коммуникацию

Эти паттерны решают задачи эффективного взаимодействия между объектами, распределяя обязанности и алгоритмы.

Observer (Наблюдатель)

Создаёт механизм подписки, позволяющий одним объектам следить за изменениями в других. Основа событийно-ориентированного программирования.

Когда использовать: обновление UI при изменении данных, системы уведомлений, обработка событий.

Пример:

#include <vector>
#include <algorithm>

class Observer {
public:
    virtual void update(const std::string& message) = 0;
    virtual ~Observer() = default;
};

class Subject {
private:
    std::vector<Observer*> observers;
public:                

Блоги

Рекомендаций по книгам