Введение: зачем нужны паттерны проектирования в C++
Паттерны проектирования (design patterns) — это проверенные решения типовых задач, возникающих при разработке программного обеспечения. В C++, с его мощной системой типов, RAII и множественным наследованием, паттерны приобретают особую гибкость. Они помогают сделать код более читаемым, поддерживаемым и масштабируемым.
В этой статье мы рассмотрим 4 ключевых паттерна из каталога GoF (Gang of Four): Singleton, Factory Method, Observer и Strategy. Каждый паттерн будет проиллюстрирован реальным примером кода на C++17/20.
Порождающие паттерны: Singleton (Одиночка)
Singleton гарантирует, что у класса существует только один экземпляр, и предоставляет глобальную точку доступа к нему. В C++ современный подход использует локальную статическую переменную (thread-safe с C++11).
Пример: Логгер приложения
class Logger {
public:
// Удаляем копирование и присваивание
Logger(const Logger&) = delete;
Logger& operator=(const Logger&) = delete;
static Logger& getInstance() {
static Logger instance; // thread-safe с C++11
return instance;
}
void log(const std::string& message) {
std::lock_guard<std::mutex> lock(m_mutex);
std::cout << "[LOG] " << message << std::endl;
}
private:
Logger() = default; // приватный конструктор
~Logger() = default;
std::mutex m_mutex;
};
// Использование
int main() {
Logger::getInstance().log("Приложение запущено");
Logger::getInstance().log("Ошибка: файл не найден");
return 0;
}
Плюсы: гарантия единственности, ленивая инициализация, потокобезопасность.
Минусы: усложняет тестирование (глобальное состояние), нарушает принцип единственной ответственности.
Порождающие паттерны: Factory Method (Фабричный метод)
Factory Method определяет интерфейс для создания объекта, но позволяет подклассам решать, какой класс инстанциировать. В C++ часто реализуется через виртуальные функции и умные указатели.
Пример: Генерация разных типов документов
// Абстрактный продукт
class Document {
public:
virtual void open() = 0;
virtual void save() = 0;
virtual ~Document() = default;
};
// Конкретные продукты
class PDFDocument : public Document {
public:
void open() override { std::cout << "Открыт PDF\n"; }
void save() override { std::cout << "Сохранен PDF\n"; }
};
class WordDocument : public Document {
public:
void open() override { std::cout << "Открыт Word\n"; }
void save() override { std::cout << "Сохранен Word\n"; }
};
// Создатель (Creator)
class Application {
public:
virtual std::unique_ptr<Document> createDocument() = 0;
virtual ~Application() = default;
void newDocument() {
auto doc = createDocument();
doc->open();
// работа с документом
}
};
// Конкретные создатели
class PDFApplication : public Application {
public:
std::unique_ptr<Document> createDocument() override {
return std::make_unique<PDFDocument>();
}
};
class WordApplication : public Application {
public:
std::unique_ptr<Document> createDocument() override {
return std::make_unique<WordDocument>();
}
};
// Использование
int main() {
std::unique_ptr<Application> app = std::make_unique<PDFApplication>();
app->newDocument(); // Вывод: "Открыт PDF"
return 0;
}
Этот паттерн идеален, когда заранее неизвестны точные типы создаваемых объектов.
Поведенческие паттерны: Observer (Наблюдатель)
Observer определяет зависимость "один ко многим" между объектами так, что при изменении состояния одного объекта все зависящие от него уведомляются и обновляются автоматически.
Пример: Система уведомлений о погоде
#include <vector>
#include <memory>
#include <algorithm>
// Интерфейс наблюдателя
class IObserver {
public:
virtual void update(float temperature, float humidity) = 0;
virtual ~IObserver() = default;
};
// Наблюдаемый субъект
class WeatherStation {
private:
std::vector<std::weak_ptr<IObserver>> observers;
float temperature;
float humidity;
public:
void addObserver(std::shared_ptr<IObserver> observer) {
observers.push_back(observer);
}
void removeObserver(std::shared_ptr<IObserver> observer) {
auto it = std::remove_if(observers.begin(), observers.end(),
[&observer](const std::weak_ptr<IObserver>& wp) {
return wp.lock() == observer;
});
observers.erase(it, observers.end());
}
void setMeasurements(float temp, float hum) {
temperature = temp;
humidity = hum;
notifyObservers();
}
private:
void notifyObservers() {
for (auto& weakObs : observers) {
if (auto obs = weakObs.lock()) {
obs->update(temperature, humidity);
}
}
}
};
// Конкретный наблюдатель
class Display : public IObserver, public std::enable_shared_from_this<Display> {
public:
void update(float temperature, float humidity) override {
std::cout << "Температура: " << temperature
<< "°C, Влажность: " << humidity << "%\n";
}
};
// Использование
int main() {
WeatherStation station;
auto display = std::make_shared<Display>();
station.addObserver(display);
station.setMeasurements(25.5, 60.0); // Вывод: Температура: 25.5°C, Влажность: 60%
return 0;
}
Важно: используйте std::weak_ptr для хранения наблюдателей, чтобы избежать утечек памяти и циклических зависимостей.
Поведенческие паттерны: Strategy (Стратегия)
Strategy определяет семейство алгоритмов, инкапсулирует каждый из них и делает их взаимозаменяемыми. Позволяет изменять алгоритм независимо от клиентов, которые его используют.
Пример: Разные способы сортировки
#include <functional>
#include <vector>
#include <algorithm>
// Интерфейс стратегии
class SortStrategy {