Введение
В статьях QML-цикла разбирала States/Transitions QML как декларативный механизм управления состояниями интерфейса — паттерн State даёт концептуально аналогичный, но реализуемый непосредственно на C++ подход к структурированию кода, поведение которого существенно зависит от текущего, дискретного состояния объекта, заменяя разрастающиеся условные конструкции (switch по текущему состоянию) явной иерархией классов состояний.
Концепция
State инкапсулирует поведение, специфичное для каждого конкретного состояния объекта, в отдельный класс, реализующий общий интерфейс состояния — объект, поведение которого зависит от состояния (Context), хранит указатель на текущий объект состояния и делегирует ему вызовы операций, специфичных для конкретного состояния, причём сам объект состояния может инициировать переход к другому состоянию (изменяя указатель Context на текущее состояние), что избегает необходимости в больших, разрастающихся условных конструкциях, выбирающих поведение в зависимости от текущего, явно хранимого как обычное поле (например, enum) состояния.
Пример кода
#include <iostream>
#include <memory>
class TrafficLightContext; // предварительное объявление
class TrafficLightState
{
public:
virtual void handle(TrafficLightContext &context) = 0;
virtual std::string name() const = 0;
virtual ~TrafficLightState() = default;
};
class TrafficLightContext // Context — делегирует поведение ТЕКУЩЕМУ состоянию, не содержит switch/if напрямую
{
public:
explicit TrafficLightContext(std::unique_ptr<TrafficLightState> initialState)
: m_state(std::move(initialState)) {}
void next() { m_state->handle(*this); } // делегирование — КОНКРЕТНОЕ поведение знает только сам State
void setState(std::unique_ptr<TrafficLightState> newState) { m_state = std::move(newState); }
std::string currentStateName() const { return m_state->name(); }
private:
std::unique_ptr<TrafficLightState> m_state;
};
class RedState : public TrafficLightState
{
public:
void handle(TrafficLightContext &context) override; // реализация ПОСЛЕ объявления остальных классов
std::string name() const override { return "Красный"; }
};
class GreenState : public TrafficLightState
{
public:
void handle(TrafficLightContext &context) override;
std::string name() const override { return "Зелёный"; }
};
void RedState::handle(TrafficLightContext &context)
{
std::cout << "Переход: Красный -> Зелёный" << std::endl;
context.setState(std::make_unique<GreenState>()); // ИМЕННО State решает, КАКОЙ будет следующее состояние
}
void GreenState::handle(TrafficLightContext &context)
{
std::cout << "Переход: Зелёный -> Красный" << std::endl;
context.setState(std::make_unique<RedState>());
}
int main()
{
TrafficLightContext light(std::make_unique<RedState>());
std::cout << "Текущее состояние: " << light.currentStateName() << std::endl;
light.next(); // Красный -> Зелёный
std::cout << "Текущее состояние: " << light.currentStateName() << std::endl;
light.next(); // Зелёный -> Красный
return 0;
}
Пояснения к коду
TrafficLightContext::next(), просто вызывающий m_state->handle(*this), показывает суть паттерна — сам Context не содержит логики «если текущее состояние красное, переключиться на зелёное» (что было бы естественно выражено через switch, для небольшого количества состояний), эта логика инкапсулирована в каждом конкретном классе состояния (RedState::handle, GreenState::handle), который сам решает, к какому следующему состоянию перейти, вызывая context.setState(...). Эта структура особенно ценна для автоматов с большим количеством состояний и сложными, специфичными для каждого состояния правилами переходов — добавление нового состояния означает создание нового класса с соответствующей логикой, без необходимости изменять и разрастающийся switch где-либо ещё в коде Context, что соответствует принципу открытости/закрытости, упомянутому про Factory Method.
Подводные камни
- Применение полной инфраструктуры State (отдельные классы для каждого состояния) для простого автомата с малым, стабильным количеством состояний, где обычный
enum(статья 523 цикла синтаксиса) сswitch-конструкцией был бы проще и достаточен — аналогично общему принципу статьи 585, для небольшого, не предполагающего частого расширения набора состояний (как простой светофор из примера с лишь двумя состояниями) простойswitchможет быть более прямым, понятным решением без введения дополнительной иерархии классов. - Создание циклических зависимостей между классами состояний без правильного использования предварительных объявлений (про forward declaration) — как показано в примере с
class TrafficLightContext;перед определениемTrafficLightState, взаимные ссылки между Context и его состояниями (Context хранит указатель на State, States ссылаются на Context в своих методах) требуют аккуратной организации объявлений и определений, разнесённых по разным точкам файла или между заголовочным файлом и файлом реализации. - Хранение в Context дополнительного, специфичного для конкретного состояния поля данных, доступного и используемого ВСЕМИ объектами состояния независимо от того, релевантно ли это поле для конкретного состояния — это нарушает инкапсуляцию паттерна, и данные, специфичные для логики конкретного состояния, по возможности лучше хранить в самом объекте этого состояния (или передавать явно через параметры метода
handle()), а не в общем, разделяемом между всеми состояниями поле Context. - Игнорирование потокобезопасности при изменении текущего состояния Context из нескольких потоков одновременно (основной цикл статей про многопоточность) — операция
setState(), изменяющая внутренний указатель на текущее состояние, без явной синхронизации может привести к состоянию гонки при одновременном вызове из нескольких потоков, особенно если переход состояния инициируется асинхронно (например, в ответ на сетевое событие, происходящее в отдельном потоке), и для многопоточного использования паттерна State нужна явная защита доступа к текущему состоянию Context.