Конечные автоматы с состояниями, у которых есть история -- это прикольная штука. Пока с этим не сталкиваешься, то думаешь: "Ну а что в этом такого?" А вот когда сталкиваешься... Тогда думаешь, что получить такой же эффект какими-то другими средствами было бы, наверное, совсем не просто.
Для начала немного вводной информации для тех, кто раньше про историю состояний не слышал.
История у состояний бывает двух типов: shallow и deep (эти термины гораздо лучше, чем русскоязычные "поверхностная" или "глубокая" история). Shallow-history хранит информацию только об одном уровне активных подсостояний. Например, если есть состояние A с shallow-history и с подсостояниями B, C и D, то A будет помнить лишь о том, какое из его непосредственных подсостояний было активно в последний раз: B, С или D. Если, скажем, в подсостоянии B есть еще несколько подсостояний (B1, B2,...), то активность этих состояний в истории состояния A не отражается. Т.е., если произошел переход в состояние B1, то в истории A будет отражена только активность состояния B, но не B1.
В случае же deep-history для A в истории будет сохраняться информация о любом активном подсостоянии A, вне зависимости от того, на какой глубине вложенности оно находится. Так, если у A есть подсостояние B, а у B есть подсостояние B1, то при переходе в B1 в истории A будет сохранена информация об активности B1.
История используется при повторном входе в состояние. Допустим, мы входим в состояние B1 (можно сказать, что мы вошли в состояние A.B.B1), затем переходим в какое-то состояние X. После чего возвращаемся в состояние A. В случае с shallow-history мы автоматически попадем в состояние B (т.е. в A.B). В случае с deep-history мы автоматически попадем в состояние B1 (т.е. в A.B.B1). Временами такое восстановление состояния по истории очень удобно, т.к. дает возможность продолжить работу конечного автомата с той точки, в которой эта работа была приостановлена.
В качестве демонстрации истории состояний в новую версию SO-5.5.15 включен пример state_deep_history, в котором задействуется состояние с deep-историей. Он так же эксплуатирует имитацию домофона, но в данном случае акцент сделан на контроль того, что пользователь вводит в консоли домофона.
Итак, пользователь может:
- ввести последовательность "dddB" (т.е. три цифры, затем кнопка "B" (Bell, т.е. звонок)), эта последовательность должна инициировать звонок в квартиру с номером "ddd";
- ввести последовательность "#ddd#ddddB" (т.е. кнопка "#", затем три цифры номера квартиры, затем "#", затем четыре цифры секретного кода этой квартиры, затем "Bell"), эта последовательность отпирает дверь подъезда при правильном вводе секретного кода;
- ввести последовательность "##ddddd#" (т.е. два раза кнопка "#", затем пять цифр сервисного кода домофона, затем еще раз "#"), эта последовательность отпирает дверь подъезда при правильном вводе сервисного кода.
Пользователь может ошибаться. Например, ввести две цифры номера квартиры вместо трех или набрать последовательность "#dddB", которая не является разрешенной. При таких ошибках нужно вывести сообщение, пока это сообщение отображается, работа с консолью блокируется.
Для демонстрации этой задачи используется небольшой КА, который выглядит приблизительно вот так:
Пожалуй, единственная важная поправочка к рисунку -- это отсутствие переходов в состояние show_error. Эти переходы выполняются из многих подсостояний состояния dialog, но я их не показывал, дабы не усложнять схему множеством однотипных линий.
Можно заметить, что состояние dialog помечено специальным маркером H*. Этот маркер указывает, что состояние dialog -- это состояние с deep-history. Т.е. dialog будет помнить о том, какое из его подсостояний, вне зависимости от вложенности, было активно в последний раз.
Наличие deep-history у состояния dialog делает работу описанного выше КА тривиальной. Когда в каком-то из подсостояний dialog обнаруживается допущенная пользователем ошибка, происходит переход в состояние show_error. В состоянии show_error описание ошибки отображается на экране, а сама консоль блокируется на 2 секунды (нажатия на кнопки игнорируются). Спустя 2 секунды происходит возврат из show_error в dialog. А т.к. у dialog-а есть deep-history, то возврат производится именно в то подсостояние, из которого мы ушли. Например, если ошибка была обнаружена внутри состояния user_code_apartment_number, то и возврат произойдет именно в него.
Все, что описано выше, уже работает. Полный текст примера можно увидеть в репозитории. Здесь же покажу всего два фрагмента, относящихся к описанию КА средствами SO-5.5.15.
Первый фрагмент -- это декларация состояний, которые входят в данный КА:
class console final : public so_5::agent_t { state_t dialog{ this, "dialog", deep_history }, wait_activity{ initial_substate_of{ dialog }, "wait_activity" }, number_selection{ substate_of{ dialog }, "number_selection" }, special_code_selection{ substate_of{ dialog }, "special_code_selection" }, special_code_selection_0{ initial_substate_of{ special_code_selection }, "special_code_selection_0" }, user_code_selection{ substate_of{ special_code_selection }, "user_code_selection" }, user_code_apartment_number{ initial_substate_of{ user_code_selection }, "apartment_number" }, user_code_secret{ substate_of{ user_code_selection }, "secret_code" }, service_code_selection{ substate_of{ special_code_selection }, "service_code" }, operation_completed{ substate_of{ dialog }, "op_completed" }, show_error{ this, "error" } ; |
Второй фрагмент -- это декларация того, что будет обрабатываться в каждом из состояний:
console( context_t ctx ) : so_5::agent_t{ ctx } { dialog .event( &console::dialog_on_grid ) .event( &console::dialog_on_cancel ); wait_activity .on_enter( &console::wait_activity_on_enter ) .transfer_to_state< key_digit >( number_selection ); number_selection .on_enter( &console::apartment_number_on_enter ) .event( &console::apartment_number_on_digit ) .event( &console::apartment_number_on_bell ) .event( &console::apartment_number_on_grid ); special_code_selection_0 .transfer_to_state< key_digit >( user_code_selection ) .just_switch_to< key_grid >( service_code_selection ); user_code_apartment_number .on_enter( &console::user_code_apartment_number_on_enter ) .event( &console::apartment_number_on_digit ) .event( &console::user_code_apartment_number_on_bell ) .event( &console::user_code_apartment_number_on_grid ); user_code_secret .on_enter( &console::user_code_secret_on_enter ) .event( &console::user_code_secret_on_digit ) .event( &console::user_code_secret_on_bell ) .event( &console::user_code_secret_on_grid ); service_code_selection .on_enter( &console::service_code_on_enter ) .event( &console::service_code_on_digit ) .event( &console::service_code_on_bell ) .event( &console::service_code_on_grid ); operation_completed .on_enter( &console::op_completed_on_enter ) .time_limit( std::chrono::seconds{3}, wait_activity ); show_error .on_enter( &console::show_error_on_enter ) .on_exit( &console::show_error_on_exit ) .time_limit( std::chrono::seconds{2}, dialog ); } |
В этих фрагментах, пожалуй, собрано все, что есть в SO-5.5.15 для поддержки иерархических КА: простые и композитные состояния, история для состояний, on_enter/on_exit, transfer_to_state и just_switch_to, а так же time_limit. Имхо, для первого приближения вполне достатояно. Тем более, что по некоторым вещам из области ИКА не понятно, что делать. Надеюсь, что какие-то дополнительные идеи появятся уже после релиза версии 5.5.15 и их реализация войдет в последующие версии.
Напоследок покажу еще кусочек трейса работы программы, по которому видно, как доставляются сообщения и как агент переходит из одного состояния в другое (трейс получен обычными шатными средствами механизма msg_tracing):
[tid=6948][agent_ptr=0x1fdf00ae5d0] state.entering [state=dialog.wait_activity]
[tid=6948][agent_ptr=0x1fdf00ae5d0] demand_handler_on_message.find_handler [mbox_id=4][msg_type=struct key_grid][signal][state=dialog.wait_activity][evt_handler=0x1fdf009d3a0]
[tid=6948][agent_ptr=0x1fdf00ae5d0] state.leaving [state=dialog.wait_activity]
[tid=6948][agent_ptr=0x1fdf00ae5d0] state.entering [state=dialog.special_code_selection.special_code_selection_0]
[tid=6948][agent_ptr=0x1fdf00ae5d0] demand_handler_on_message.find_handler [mbox_id=4][msg_type=struct key_digit][envelope_ptr=0x1fdf00a8340][payload_ptr=0x1fdf00a8350][state=dialog.special_code_selection.special_code_selection_0][evt_handler=0x1fdf009d550]
[tid=6948][agent_ptr=0x1fdf00ae5d0] state.leaving [state=dialog.special_code_selection.special_code_selection_0]
[tid=6948][agent_ptr=0x1fdf00ae5d0] state.entering [state=dialog.special_code_selection.user_code_selection.apartment_number]
[tid=6948][agent_ptr=0x1fdf00ae5d0] demand_handler_on_message.find_handler [mbox_id=4][msg_type=struct key_digit][envelope_ptr=0x1fdf00a8340][payload_ptr=0x1fdf00a8350][state=dialog.special_code_selection.user_code_selection.apartment_number][evt_handler=0x1fdf009d5e0]
[tid=6948][agent_ptr=0x1fdf00ae5d0] demand_handler_on_message.find_handler [mbox_id=4][msg_type=struct key_digit][envelope_ptr=0x1fdf00a84c0][payload_ptr=0x1fdf00a84d0][state=dialog.special_code_selection.user_code_selection.apartment_number][evt_handler=0x1fdf009d5e0]
[tid=6948][agent_ptr=0x1fdf00ae5d0] demand_handler_on_message.find_handler [mbox_id=4][msg_type=struct key_bell][signal][state=dialog.special_code_selection.user_code_selection.apartment_number][evt_handler=0x1fdf009d790]
[tid=6948][agent_ptr=0x1fdf00ae5d0] state.leaving [state=dialog.special_code_selection.user_code_selection.apartment_number]
[tid=6948][agent_ptr=0x1fdf00ae5d0] state.entering [state=error]
[tid=6948][agent_ptr=0x1fdf00ae5d0] demand_handler_on_message.find_handler [mbox_id=5][msg_type=struct so_5::state_t::time_limit_t::timeout][signal][state=error][evt_handler=0x1fdf009dd30]
[tid=6948][agent_ptr=0x1fdf00ae5d0] state.leaving [state=error]
[tid=6948][agent_ptr=0x1fdf00ae5d0] state.entering [state=dialog.special_code_selection.user_code_selection.apartment_number]
Комментариев нет:
Отправить комментарий