В моделях данных очень часто требуется хранить некоторые переключаемые состояния. Классический способ в С++ для этого — использование перечислимых типов enum.
Например, если у вас в программе пользователь может переключаться между двумя экранами, вы заводите enum screen { screen_one, screen_two }; и переменную screen cur_screen_. Отрисовщик должен получить у модели «текущий выбранный экран», и затем отрисовать его, запрашивая у модели дополнительные данные, относящиеся именно к этому экрану. Что-то вроде:
При использовании такой модели, программист может запрашивать данные, которые для текущего состояния совершенно не актуальны. Например, вызвать метод get_screen_two_elements() для получения списка элементов второго экрана, когда текущий экран — первый. Хорошей практикой является использование ассертов вида ASSERT(cur_screen_ == screen_one) в методах, зависимых от конкретного экрана. Это обеспечивает некоторый контроль времени выполнения.
Но есть способ обеспечить контроль времени компиляции и более явное разделение состояний с помощью boost::variant.
При таком подходе screen_one и screen_two — это не элементы enum'a, а полноценные классы. И все, зависимые от этого состояния данные и методы — переходят внутрь класса состояния.
Больше нет метода get_screen_one_elements() в основной модели, теперь есть метод get_elements() у класса screen_one. Текущий выбранный экран сохраняется в переменной типа boost::variant<screen_one, screen_two>.
Для восприятия (отрисовки) такой модели нужно использовать механизм visitor'а. Это особый функтор, который определяет операторы скобочки для каждого элемента из варианта. В нашем случае — для каждого состояния.
Класс-состояние хранит в себе данные, необходимые на время нахождения экрана в «выбранном состоянии» и обеспечивает всю специфическую функциональность. Вы никак не обратитесь к функциональности второго экрана, если текущим выбран первый.
Вообще, концепция визиторов очень напоминает по духу операцию «измерений» в физике. Есть некая система, мы применяем к ней инструмент-измеритель. Получаем результат, и/или изменяем состояние системы.
Кроме того, очень удобно использовать визиторы для языковых компиляторов. Разбором получаем AST (дерево элементов), затем применяем к нему разнообразные инструменты для анализа, оптимизации, и результирующей выдачи.
Например, если у вас в программе пользователь может переключаться между двумя экранами, вы заводите enum screen { screen_one, screen_two }; и переменную screen cur_screen_. Отрисовщик должен получить у модели «текущий выбранный экран», и затем отрисовать его, запрашивая у модели дополнительные данные, относящиеся именно к этому экрану. Что-то вроде:
switch (model.cur_screen())
{
case screen_one:
model.get_screen_one_elements();
...
case screen_two:
model.get_screen_two_elements();
...
}
При использовании такой модели, программист может запрашивать данные, которые для текущего состояния совершенно не актуальны. Например, вызвать метод get_screen_two_elements() для получения списка элементов второго экрана, когда текущий экран — первый. Хорошей практикой является использование ассертов вида ASSERT(cur_screen_ == screen_one) в методах, зависимых от конкретного экрана. Это обеспечивает некоторый контроль времени выполнения.
Но есть способ обеспечить контроль времени компиляции и более явное разделение состояний с помощью boost::variant.
При таком подходе screen_one и screen_two — это не элементы enum'a, а полноценные классы. И все, зависимые от этого состояния данные и методы — переходят внутрь класса состояния.
Больше нет метода get_screen_one_elements() в основной модели, теперь есть метод get_elements() у класса screen_one. Текущий выбранный экран сохраняется в переменной типа boost::variant<screen_one, screen_two>.
class screen_one
{
public:
const std::vector<screen_one_elements>& get_elements() const
{
return ...;
}
};
class screen_two
{
public:
const std::vector<screen_two_elements>& get_elements() const
{
return ...;
}
};
class cool_data_model
{
public:
typedef boost::variant<screen_one, screen_two> screen;
template<typename NewScreenType>
void change_screen(const NewScreenType& new_val)
{
cur_screen_ = new_val;
}
template<typename VisitorType>
VisitorType::result_type apply_visitor(const VisitorType& visitor)
{
return boost::apply_visitor(visitor, cur_screen_);
}
private:
screen cur_screen_;
};
Для восприятия (отрисовки) такой модели нужно использовать механизм visitor'а. Это особый функтор, который определяет операторы скобочки для каждого элемента из варианта. В нашем случае — для каждого состояния.
class painter : public boost::static_visitor<>
{
public:
void operator()(const screen_one& val_screen)
{
// рисуем первый экран, получая необходимые данные из его модели
val_screen.get_elements();
...
}
void operator()(const screen_two& val_screen)
{
// рисуем второй экран, получая необходимые данные из его модели
val_screen.get_elements();
...
}
};
model.apply_visitor(painter());
Класс-состояние хранит в себе данные, необходимые на время нахождения экрана в «выбранном состоянии» и обеспечивает всю специфическую функциональность. Вы никак не обратитесь к функциональности второго экрана, если текущим выбран первый.
Вообще, концепция визиторов очень напоминает по духу операцию «измерений» в физике. Есть некая система, мы применяем к ней инструмент-измеритель. Получаем результат, и/или изменяем состояние системы.
Кроме того, очень удобно использовать визиторы для языковых компиляторов. Разбором получаем AST (дерево элементов), затем применяем к нему разнообразные инструменты для анализа, оптимизации, и результирующей выдачи.