Pull to refresh

Использование boost::variant для описания состояний модели

Reading time3 min
Views10K
В моделях данных очень часто требуется хранить некоторые переключаемые состояния. Классический способ в С++ для этого — использование перечислимых типов enum.

Например, если у вас в программе пользователь может переключаться между двумя экранами, вы заводите 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 (дерево элементов), затем применяем к нему разнообразные инструменты для анализа, оптимизации, и результирующей выдачи.
Tags:
Hubs:
Total votes 24: ↑22 and ↓2+20
Comments17

Articles