Почему 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) {