Продвинутые паттерны проектирования в C++: от теории к сложным реализациям
Паттерны проектирования — это проверенные решения типовых задач, которые возникают при разработке программного обеспечения. Если базовые паттерны (Singleton, Factory, Observer) знакомы каждому разработчику, то продвинутые техники требуют глубокого понимания языка C++, его шаблонов, множественного наследования и метапрограммирования. В этой статье мы разберём четыре мощных паттерна: Visitor, Strategy, State и CRTP — с реальными примерами кода и объяснением, когда их применять.
1. Паттерн Visitor (Посетитель) — расширение функциональности без изменения классов
Паттерн Visitor позволяет добавлять новые операции к иерархии классов, не изменяя сами классы. Это особенно полезно, когда у вас есть стабильная структура объектов, но часто меняющиеся алгоритмы их обработки.
Пример: обработка геометрических фигур
#include <iostream>
#include <vector>
#include <memory>
// Forward declaration
class Circle;
class Rectangle;
// Абстрактный посетитель
class Visitor {
public:
virtual void visit(Circle& circle) = 0;
virtual void visit(Rectangle& rectangle) = 0;
virtual ~Visitor() = default;
};
// Абстрактный элемент
class Shape {
public:
virtual void accept(Visitor& visitor) = 0;
virtual ~Shape() = default;
};
// Конкретные элементы
class Circle : public Shape {
public:
double radius;
explicit Circle(double r) : radius(r) {}
void accept(Visitor& visitor) override {
visitor.visit(*this);
}
};
class Rectangle : public Shape {
public:
double width, height;
Rectangle(double w, double h) : width(w), height(h) {}
void accept(Visitor& visitor) override {
visitor.visit(*this);
}
};
// Конкретный посетитель — вычисление площади
class AreaVisitor : public Visitor {
public:
double totalArea = 0.0;
void visit(Circle& circle) override {
totalArea += 3.14159 * circle.radius * circle.radius;
}
void visit(Rectangle& rectangle) override {
totalArea += rectangle.width * rectangle.height;
}
};
int main() {
std::vector<std::unique_ptr<Shape>> shapes;
shapes.push_back(std::make_unique<Circle>(5.0));
shapes.push_back(std::make_unique<Rectangle>(4.0, 6.0));
AreaVisitor areaCalc;
for (auto& shape : shapes) {
shape->accept(areaCalc);
}
std::cout << "Total area: " << areaCalc.totalArea << std::endl;
return 0;
}
Когда использовать: когда нужно выполнять разные операции над объектами сложной иерархии, не изменяя их код. Недостаток — добавление нового типа требует изменения всех посетителей.
2. Паттерн Strategy (Стратегия) — гибкая смена алгоритмов на лету
Strategy инкапсулирует семейство взаимозаменяемых алгоритмов и позволяет подменять их во время выполнения. В C++ это часто реализуется через std::function или виртуальные классы.
Пример: система сортировки данных
#include <iostream>
#include <vector>
#include <algorithm>
#include <memory>
#include <functional>
// Контекст, использующий стратегию
class Sorter {
private:
std::function<void(std::vector<int>&)> strategy_;
public:
explicit Sorter(std::function<void(std::vector<int>&)> strategy)
: strategy_(std::move(strategy)) {}
void setStrategy(std::function<void(std::vector<int>&)> newStrategy) {
strategy_ = std::move(newStrategy);
}
void sort(std::vector<int>& data) {
if (strategy_) {
strategy_(data);
}
}
};
// Конкретные стратегии
void bubbleSort(std::vector<int>& data) {
for (size_t i = 0; i < data.size(); ++i) {
for (size_t j = 0; j < data.size() - i - 1; ++j) {
if (data[j] > data[j + 1]) {
std::swap(data[j], data[j + 1]);
}
}
}
}
void quickSort(std::vector<int>& data) {
std::sort(data.begin(), data.end());
}
int main() {
std::vector<int> numbers = {5, 3, 8, 1, 2};
Sorter sorter(bubbleSort);
sorter.sort(numbers); // Используем пузырьковую сортировку
sorter.setStrategy(quickSort);
sorter.sort(numbers); // Переключаемся на быструю сортировку
for (int n : numbers) {
std::cout << n << " ";
}
return 0;
}
Когда использовать: когда у вас есть несколько вариантов алгоритмов, которые могут меняться динамически. Отлично сочетается с Dependency Injection.
3. Паттерн State (Состояние) — управление поведением объекта через состояния
State позволяет объекту изменять своё поведение при изменении внутреннего состояния. В C++ это часто реализуется через конечные автоматы.
Пример: автомат для кофемашины
#include <iostream>
#include <memory>
class CoffeeMachine; // Forward declaration
// Абстрактное состояние
class State {
public:
virtual void insertCoin(CoffeeMachine& machine) = 0;
virtual void selectDrink(CoffeeMachine& machine) = 0;
virtual void dispense(CoffeeMachine& machine) = 0;
virtual ~State() = default;
};
// Конкретные состояния
class IdleState : public State {
public:
void insertCoin(CoffeeMachine& machine) override;
void selectDrink(CoffeeMachine& machine) override;
void dispense(CoffeeMachine& machine) override;
};
class HasCoinState : public State {
public:
void insertCoin(CoffeeMachine& machine) override;
void selectDrink(CoffeeMachine& machine) override;
void dispense(CoffeeMachine& machine) override;
};
class DispensingState : public State {
public:
void insertCoin(CoffeeMachine& machine) override;
void selectDrink(CoffeeMachine& machine) override;
void dispense(CoffeeMachine& machine) override;
};
// Контекст — кофемашина
class CoffeeMachine {
private:
std::unique_ptr<State> state_;
public:
CoffeeMachine() : state_(std::make_unique<IdleState>()) {}
void setState(std::unique_ptr<State> newState) {
state_ = std::move(newState);
}
void insertCoin() { state_->insertCoin(*this); }
void selectDrink() { state_->selectDrink(*this); }
void dispense() { state_-&