Patterns. Паттерн State (Состояние). Структура. Реализация на C++

Паттерн State (Состояние). Структура. Реализация на C++


Содержание


1. Общие сведения. Назначение. Диаграмма классов

Паттерн State относится к паттернам поведения объектов и предназначен для помещения состояний объекта в отдельные объекты. В зависимости от внутреннего состояния объект изменяет свое поведение.

На рисунке 1 изображена структура паттерна State.

Паттерн State. Структура

Рисунок 1. Структура паттерна State

 

2. Участники паттерна

Согласно рисунку 1 участниками паттерна являются:

  • Client – клиент, использующий разные состояния объекта через контекст;
  • Context – класс контекста. Это класс, через который клиент имеет доступ к конкретному состоянию объекта;
  • State – класс состояния. Определение интерфейса для инкапсуляции поведения объекта. Это поведение зависит от конкретного состояния Context;
  • ConcreteStateA, ConcreteStateB – подклассы, реализующие соответствующее поведение, ассоциированное с различными состояниями контекста Context.

 

3. Отношения

Для паттерна State можно выделить следующие отношения (см. рисунок 1):

  • с помощью поля state класс Context делегирует запросы текущему объекту. Эти запросы зависят от выбранного состояния;
  • существует возможность передачи объекта Context в качестве аргумента в объект состояния State. В этом случае объект состояния может получить доступ к контексту;
  • основным интерфейсом для клиента является контекст (Context). Можно реализовать конфигурацию клиентами контекста конкретными объектами состояния. После этого можно удобно использовать эти объекты состояния;
  • изменение состояния может происходить двумя способами: через контекст (Context) или через подклассы конкретных состояний (ConcreteState).

 

4. Результаты

Паттерн State дает следующие результаты.

  1. Поведение объекта, которое зависит от состояния, является локализованным и разделенным на соответствующие части. Каждая часть является отдельным состоянием объекта. Новые состояния добавляются путем наследования новых подклассов.

Преимуществом паттерна State является то, что не нужно использовать условные операторы для определения того или иного состояния. Вместо этого предлагается распределение между подклассами класса State. Однако здесь отрицательным явлением может быть увеличение общего количества классов.

  1. Ввод отдельных объектов для разных состояний делает переходы между состояниями более очевидными. Эти переходы представляют собой атомарные действия. Для осуществления перехода необходимо изменить значение только одной объектной переменной state в классе Context.
  2. Есть возможность разделения объектов состояния. В объекте состояния State то или иное состояние можно кодировать соответствующим типом (классом). Тогда разные контексты могут разделять один и тот же объект State. В этом случае состояния являются приспособленцами.

 

5. Случаи применения паттерна State

Паттерн State следует использовать в следующих случаях:

  • когда во время выполнения необходимо изменять поведение объекта в зависимости от его состояния;
  • когда код содержит много ветвлений с использованием операторов условия (операторов выбора) и каждое ветвление является состоянием объекта. В этом случае в каждое разветвление помещается в отдельный класс, при котором каждое состояние трактуется как отдельный самостоятельный объект.

 

6. Пример реализации паттерна на C++

 

// Паттерн State - состояние
#include <iostream>
using namespace std;

// Интерфейс, инкапсулирующий поведение,
// которое соответствует конкретному состоянию контекста (Context)
class State abstract
{
public:
  virtual void Handle() = 0;
};

// Конкретные состояния (поведения)
class ConcreteStateA : public State
{
public:
  void Handle() override
  {
    cout << "ConcreteStateA::Handle()" << endl;
  }
};

class ConcreteStateB : public State
{
public:
  void Handle() override
  {
    cout << "ConcreteStateB::Handle()" << endl;
  }
};

// Класс контекста
class Context
{
private:
  State* state = nullptr;

public:
  // Конструктор
  Context(State* state = nullptr) : state(state)
  { }

  void Request()
  {
    state->Handle();
  }
};

void main()
{
  // Клиентский код
  // 1. Создать конкретные состояния
  State* state1 = new ConcreteStateA();
  State* state2 = new ConcreteStateB();

  // 2. Создать контекст
  Context* context = nullptr;

  // 3. Создать меню выбора состояния
  int stateNum;
  cout << "stateNum = ";
  cin >> stateNum;

  // задать состояние объекта
  if (stateNum == 1)
    context = new Context(state1);
  else
    context = new Context(state2);

  // 4. Вывести значение состояния
  context->Request();

  // 5. Освободить память
  if (state1)
    delete state1;
  if (state2)
    delete state2;
  if (context)
    delete context;
}

 

7. Реализация

При реализации паттерна State возникают следующие вопросы.

  1. Нет четкой информации о том, кто реализует переход между состояниями. Это может быть контекст, а могут быть подклассы класса State. В этом случае более гибким подходом считается использование подклассов состояния, для которых должен быть реализован соответствующий интерфейс.
  2. Для представления изменения состояний можно употреблять таблицы. Эти таблицы производят отображение входных данных на переходы между состояниями. С помощью этого способа определяется, в какое состояние нужно перейти при получении некоторых входных данных. Таким образом, виртуальные функции заменяются поиском в таблице.
  3. Выбор способности создания (уничтожения) объектов в процессе разработки. Создавать объекты можно по необходимости и в начале. При первом способе объекты уничтожаются сразу после использования. При втором способе объекты создаются в начале программы и уничтожаются в самом конце при завершении программы.

Первый вариант считается предпочтительным в случаях, когда не известно, в какое состояние будет попадать система. Здесь некоторые объекты могут быть вообще не использованы, а на их создание выделились ресурсы.

Второй способ предпочтительнее в случаях когда изменения состояния происходят часто. При этом экономится время на многократное создание/уничтожение объекта.

  1. Использование динамического наследования. Здесь предполагается изменение поведения по запросу (класс объекта изменяется при выполнении). Но некоторые языки программирования эту опцию не поддерживают.

 


Связанные темы