Архитектурные паттерны в C++: как писать код, который не стыдно показать

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

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

Начать курс

Почему C++ без архитектуры — это боль

Если вы когда-нибудь открывали чужой (или свой старый) C++ проект и чувствовали, как глаза начинают слезиться от тысяч строк в одном файле, а компиляция превращается в ритуал с чашкой кофе — вы не одиноки. C++ даёт огромную свободу: можно смешивать стили, перегружать операторы, делать шаблонную магию. Но свобода без дисциплины ведёт к хаосу.

Архитектурные паттерны в C++ — это не скучная теория из университетских лекций. Это проверенные боевые рецепты, которые спасают проект, когда он вырастает из «игрушечного» в промышленный. В этой статье мы разберём архитектурные паттерны C++, которые реально упрощают жизнь: от MVC до Observer. И да, будет много кода.

Что такое архитектурный паттерн (и чем он отличается от «просто паттерна»)

Многие путают паттерны проектирования C++ (GoF: Singleton, Factory, Adapter) с архитектурными. Разница проста:

  • Паттерны проектирования — микро-решения уровня класса или модуля (например, как создать объект).
  • Архитектурные паттерны — макро-решения уровня всего приложения (как компоненты общаются, как разделены слои).

Архитектура определяет, не превратится ли ваш проект в «большой комок грязи» (Big Ball of Mud) через полгода. Поэтому давайте посмотрим на три самых полезных паттерна для C++.

1. MVC (Model-View-Controller) в C++: разделяй и властвуй

MVC — классика. Он разделяет приложение на три части: данные (Model), отображение (View) и логику управления (Controller). В C++ это особенно актуально для GUI-приложений (Qt, wxWidgets) или даже консольных утилит.

Как это выглядит в коде

Представим простейший калькулятор. Модель хранит результат, View выводит его, Controller обрабатывает ввод.

#include <iostream>
#include <string>

// Model: ничего не знает о GUI
class CalculatorModel {
public:
    double add(double a, double b) {
        result_ = a + b;
        return result_;
    }
    double getResult() const { return result_; }
private:
    double result_ = 0;
};

// View: только отображение
class CalculatorView {
public:
    void showResult(double value) {
        std::cout << "Результат: " << value << std::endl;
    }
    void showError(const std::string& msg) {
        std::cerr << "Ошибка: " << msg << std::endl;
    }
};

// Controller: связывает Model и View
class CalculatorController {
public:
    CalculatorController(CalculatorModel& model, CalculatorView& view)
        : model_(model), view_(view) {}

    void handleAdd(double a, double b) {
        double res = model_.add(a, b);
        view_.showResult(res);
    }
private:
    CalculatorModel& model_;
    CalculatorView& view_;
};

int main() {
    CalculatorModel model;
    CalculatorView view;
    CalculatorController controller(model, view);

    controller.handleAdd(5, 3); // Вывод: Результат: 8
    return 0;
}

Почему это круто? Вы можете заменить консольную View на Qt-виджет, не трогая Model. Или добавить юнит-тесты для Model без запуска GUI. Это основа проектирования C++ приложений с нуля.

2. Observer (Наблюдатель): когда изменения должны быть заметны всем

Представьте: у вас есть датчик температуры, и несколько панелей должны обновляться, когда температура меняется. Вместо того чтобы опрашивать датчик каждую секунду (polling), Observer сам уведомляет подписчиков. Это архитектурный паттерн C++, который делает код событийно-ориентированным.

Реализация на C++11 и новее

Используем умные указатели и std::function для гибкости.

#include <iostream>
#include <vector>
#include <memory>
#include <functional>

// Интерфейс наблюдателя
class IObserver {
public:
    virtual ~IObserver() = default;
    virtual void update(float temperature) = 0;
};

// Конкретный наблюдатель: дисплей
class Display : public IObserver {
public:
    explicit Display(const std::string& name) : name_(name) {}
    void update(float temperature) override {
        std::cout << name_ << ": температура " << temperature << "°C\n";
    }
private:
    std::string name_;
};

// Субъект (Observable)
class TemperatureSensor {
public:
    void attach(std::shared_ptr<IObserver> observer) {
        observers_.push_back(observer);
    }
    void setTemperature(float temp) {
        temperature_ = temp;
        notify();
    }
private:
    void notify() {
        for (auto& obs : observers_) {
            obs->update(temperature_);
        }
    }
    std::vector<std::shared_ptr<IObserver>> observers_;
    float temperature_ = 0.0f;
};

int main() {
    TemperatureSensor sensor;
    auto display1 = std::make_shared<Display>("Кухня");
    auto display2 = std::make_shared<Display>("Гостиная");

    sensor.attach(display1);
    sensor.attach(display2);

    sensor.setTemperature(23.5);
    // Вывод:
    // Кухня: температура 23.5°C
    // Гостиная: температура 23.5°C
    return 0;
}

Обратите внимание: мы используем std::shared_ptr, чтобы не заботиться об удалении. Это пример того, как паттерны проектирования C++ адаптируются под современные стандарты.

3. Strategy (Стратегия): алгоритмы как модули

Допустим, вам нужно сортировать данные разными способами в зависимости от ситуации. Вместо гигантского switch-case вы выносите каждый алгоритм в отдельный класс. Это идеальный архитектурный паттерн C++ для случаев, когда поведение должно меняться на лету.

Пример: система сжатия файлов

#include <iostream>
#include <memory>
#include <string>

// Стратегия: интерфейс алгоритма
class CompressionStrategy {
public:
    virtual ~CompressionStrategy() = default;
    virtual void compress(const std::string& file) = 0;
};

// Конкретные стратегии
class ZipCompression : public CompressionStrategy {
public:
    void compress(const std::string& file) override {
        std::cout << "Сжатие " << file << " в ZIP...\n";
    }
};

class RarCompression : public CompressionStrategy {
public:
    void compress(const std::string& file) override {
        std::cout << "Сжатие " << file << " в RAR...\n";
    }
};

// Контекст: использует стратегию
class Compressor {
public:
    explicit Compressor(std::unique_ptr<CompressionStrategy> strategy)
        : strategy_(std::move(strategy)) {}

    void setStrategy(std::unique_ptr<CompressionStrategy> strategy) {                

Блоги

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