В новой версии SO-5.5.15 реализуется поддержка иерархических конечных автоматов (об этом уже писалось). В процессе реализации оказалось, что для нормальных ИКА нужно практически все, что было описано David Harel в его основополагающей работе "Statecharts: A Visual Formalism For Complex Systems" и затем перекочевало в диаграммы состояний UML. Например: композитные состояния, обработчики входа-выхода, состояния с историями. И, среди прочего, автоматическая смена состояния после прошествия указанного времени.
В SO-5.5.15 поддержка автоматической смены состояния агента после указанного тайм-аута реализуется посредством метода time_limit для состояния агента.
В качестве демонстрации представим себе простого агента который, скажем, имитирует экран инфокиоска. У агента всего два состояния. Первое состояние dialog. В этом состоянии агент воспринимает действия пользователя (нажатие на экран, нажатия на кнопки и т.д.) и отображает какую-то информацию. Второе состояние show_err. Это состояние используется дабы вывести на экран киоска на 2 секунды какое-то сообщение об ошибке. В течении этого времени инфокиоск должен игнорировать любые действия пользователя.
На SO-5.5.15 сейчас это можно представить вот в таком виде:
class basic_infokiosk : public so_5::agent_t { protected : state_t dialog{ this, "dialog" }; state_t show_err{ this, "err" }; ... public : virtual void so_define_agent() override { // Нормальный диалог с пользователем. dialog // На входе в состояние очищаем экран. .on_enter( [this] { m_display.clear(); } ) // Обработчик очередного действия пользователя. .event( [this]( const user_input & msg ) { // Пытаемся что-то сделать. try_handle_input( msg ); if( some_error() ) { // Если не получилось, то формируем описание ошибки // и идем в состояние show_err для того, чтобы пользователь // его прочитал и начал заново. m_error = error_description(); this >>= show_err; } } ); // Отображение сообщения об ошибке. show_err // На входе в состояние отображаем на экране текст ошибки. .on_enter( [this] { m_display.show_error_message( m_error ); } ) // Ограничиваем свое пребывание в этом состоянии всего двумя // секундами после чего возвращаемся в нормальное состояние. .time_limit( std::chrono::seconds{2}, dialog ); } ... protected : display m_display; std::string m_error; ... }; |
Даже на тех небольших примерах, на которых обкатывалась реализация ИКА, оказалось, что time_limit для состояний очень удобен: пользователь освобождается от необходимости вручную работать с отложенными сообщениями, их отменой, проверкой уникальности и т.д. В общем, для более-менее сложных ИКА time_limit, как сейчас представляется, -- это must have feature.
В пару к time_limit для состояния был добавлен еще и метод drop_time_limit, который позволяет отменить заданный для состояния лимит времени.
В принципе, drop_time_limit добавлялся из соображений "шоб было". Т.е., если есть возможность задать временной лимит, то может потребоваться и возможность от этого лимита избавиться. Да и сценарий, где drop_time_limit может потребоваться, придумать не так уж и сложно. Допустим, мы используем наследование агентов. И кроме класса basic_infokiosk, который отображает сообщение об ошибке в течении 2-х секунд, у нас есть и его наследник, adv_infokiosk, который во время отображения ошибки позволяет пользователю выбрать какое-то корректирующее действие, ввести дополнительные параметры и т.д. Т.е. класс-наследник в show_err ведет свой диалог с пользователем и наследнику не нужно это ограничение на 2 секунды. Наследник может полностью отменить ограничение посредством drop_time_limit:
// Более продвинутая версия, которая не нуждается в ограничении на время // пребывания в show_err. class adv_infokiosk : public basic_infokiosk { public : virtual void so_define_agent() override { // Позволяем базовому классу провести настройку своего поведения. basic_infokiosk::so_define_agent(); // Меняем свойства состояния show_err. show_err // Лимит на время нахождения в состоянии больше не нужен. .drop_time_limit() // Какие-то действия для состояния show_err... .event...; } ... }; |
До сих пор все было более-менее просто и, надеюсь, понятно. Дальше веселее ;)
Метод drop_time_limit для состояния S можно вызывать когда агент уже находится в этом самом состоянии S. После чего автоматический переход в другое состояние отменяется полностью. Т.е. если агента из его текущего состояния S не вывести вручную, автоматически он уже никуда не перейдет.
Где это может потребоваться? Да в том же примере с инфокиоском.
Допустим, у нас есть еще один наследник basic_infokiosk, который отображает сообщение об ошибке и показывает кнопку "Подробнее...". Если пользователь ничего не делает, то выход из show_err происходит через 2 секунды. Если нажимает "Подробнее...", то нужно отобразить расширенное описание ошибки. Показывать это описание нужно не более 30 секунд.
В коде это может выглядеть вот так:
// Еще одна продвинутая версия инфокиоска, которая позволяет получить // расширенную информацию об ошибке. class extended_infokiosk : public basic_infokiosk { // Пара новых состояний, которые нам потребуются дабы расширить // поведение агента в состоянии show_err. state_t err_default{ initial_substate_of{ show_err }, "default" }; state_t err_extended{ substate_of{ show_err }, "extended" }; public : virtual void so_define_agent() override { // Позволяем базовому классу провести настройку своего поведения. basic_infokiosk::so_define_agent(); // Расширяем логику поведения агента посредством расширения // поведения агента в состоянии show_err. // Не забываем: базовый класс назначил ограничение на пребывание // в состоянии show_err не более 2-х секунд. show_err // Реакция на нажатие "Подробнее..." .event< more_info >( [this] { // Отменяем отсчет лимита времени для show_err. show_err.drop_time_limit(); // Идем в состояние err_extended для того, чтобы отобразить // расширенное описание ошибки. this >>= err_extended; } ); // Определяем поведение агента в новом состоянии. err_extended // На входе в состояние отображаем дополнительную информацию. .on_enter( [this] { m_display.show_extended_error( make_extended_info(); } ) // Ограничиваем время пребывания в этом состоянии 30 секундами. .time_limit( std:chrono::seconds{30}, dialog ) // По нажатию на "OK" возвращаемся к нормальному диалогу. .just_switch_to< ok >( dialog ); } ... }; |
Все бы ничего, но! Когда мы войдем в show_err в следующий раз, то ограничения на время пребывания в show_err уже не будет. Ведь оно было отменено вызовом drop_time_limit. Поэтому, нам нужно где-то вызывать time_limit для show_err еще раз.
И вот тут проявляется особенность, которая существует в текущей реализации SO-5.5.15: если агент находится в состоянии S и последовательно вызывает S.drop_time_limit, затем S.time_limit, то заданный таким образом лимит времени будет активирован для состояния S только при следующем входе в состояние S.
Это означает, что если мы сбросили лимит времени для show_err, вошли в show_err, установили лимит времени заново, то автоматически из show_err по истечении этого лимита мы не выйдем. Т.к. отсчет лимита времени начинается при входе в состояние. Если на момент входа в show_err лимита времени не было, то ничего не отсчитывается.
Вот такая особенность есть сейчас. И обходится она, например, вот так:
show_err // Реакция на нажатие "Подробнее..." .event< more_info >( [this] { // Отменяем отсчет лимита времени для show_err. show_err.drop_time_limit(); // И назначаем его снова, дабы он отсчитывался при // следующем входе в show_err. show_err.time_limit( std::chrono::seconds{2}, dialog ); // Идем в состояние err_extended для того, чтобы отобразить // расширенное описание ошибки. this >>= err_extended; } ); |
Либо вот так:
show_err // По нажатию на "Подробнее..." идем в специальное подсостояние, // где и будет отображаться расширенная информация об ошибке. .just_switch_to< more_info >( err_extended ); // Определяем поведение агента в новом состоянии. err_extended // На входе в состояние отображаем дополнительную информацию. // А так же отменяем лимит времени пребывания в show_err. // Нам нужна эта отмена, т.к. состояние err_extended является подстостоянием show_err. .on_enter( [this] { show_err.drop_time_limit(); m_display.show_extended_error( make_extended_info(); } ) // На выходе из состояния восстанавливаем лимит на время // пребывания в состоянии show_err. .on_exit( [this] { show_err.time_limit( std::chrono::seconds{2}, dialog ); } ) // Ограничиваем время пребывания в этом состоянии 30 секундами. .time_limit( std:chrono::seconds{30}, dialog ) // По нажатию на "OK" возвращаемся к нормальному диалогу. .just_switch_to< ok >( dialog ); |
Когда делалась текущая реализация лимитов времени для состояния, видимо, об этом просто не подумал. А когда стал писать документацию, наткнулся на такую особенность. И мне показалось, что подобное поведение для пользователей SO-5 может стать откровением.
Посему интересно мнение читателей. А что для вас было бы более логичным:
- Вызов S.time_limit когда агент уже находится в состоянии S сразу же начинает отсчет лимита времени для S (повторный вход в S не требуется);
- Вызов S.time_limit когда агент уже находится в состоянии S не начинает отсчет лимита времени для S (лимит времени будет отсчитываться только при повторном входе в S).
Есть ли у кого-нибудь какие-нибудь мнения? Или все это уже слишком мудрено?
Upd. В итоге реализация time_limit изменена: если при вызове S.time_limit() состояние S уже активно (само по себе или активно какое-то из его подсостояний), то отсчет времени пребывания агента в состоянии S начинается заново. Повторный вход в S для срабатывания временного лимита не нужен.
Кстати говоря, даже на таком тривиальном примере с классами *_infokiosk может стать заметно, что дружба модели акторов и ООП имеет некоторые специфические особенности. Поэтому тупой перенос реализации модели акторов из Erlang-а в C++ вряд ли имеет смысл. Тут нужно тоньше. Ну или толще :)
Комментариев нет:
Отправить комментарий