Давайте спробуємо поговорити про ієрархічні кінцеві автомати взагалі та їх підтримку в SObjectizer-5 зокрема

Кінцеві автомати — це, мабуть, одне із самих основних і широко використовуваних понять в програмуванні. Кінцеві автомати (КА) активно застосовуються в безлічі прикладних ніш. Зокрема, в таких нішах, як АСУТП і телеком, з якими доводилося мати справу, КА зустрічаються дещо рідше, ніж на кожному кроці.

Тому в даній статті ми спробуємо поговорити про КА, в першу чергу про ієрархічних кінцевих автоматах та їх просунутих можливості. І трохи розповісти про підтримку КА в SObjectizer-5, «акторном» фреймворку для C++. Одному з тих двухнескольких, які відкриті, безкоштовні, крос-платформенны, і все ще живі.

Навіть якщо вам не цікавий SObjectizer, але ви, скажімо, ніколи не чули про ієрархічні кінцеві автомати або про те, наскільки корисні такі просунуті можливості КА, як обробники входу-виходу для станів або історія стану, то вам може бути цікаво зазирнути під кат і прочитати хоча б першу частину статті.

Загальними словами про кінцеві автомати
Ми не будемо пробувати провести у статті повний лікнеп на тему автоматів і такий їх різновид, як кінцеві автомати. Читачеві необхідно мати хоча б базове уявлення про ці типи сутностей.

Просунуті скінченні автомати та їх можливості

У КА є кілька «просунутих» можливостей, які багаторазово підвищують зручність роботи з КА в програмі. Давайте коротко пройдемося по цим «просунутим» можливостям.

Disclaimer: якщо читач добре знайомий з діаграмами станів з UML, то нічого нового він тут для себе не знайде.

Ієрархічні кінцеві автомати

Мабуть, найбільш важлива і цінна можливість — це організація ієрархії/вкладеності станів. Оскільки саме можливість вкласти стану один в одного усуває «вибух» кількості переходів зі стану в стан за міру збільшення складності КА.

Пояснити це словами важче, ніж показати на прикладі. Тому давайте уявимо, що у нас є інфокіоск, на екрані якого спершу відображається вітальне повідомлення. Користувач може вибрати пункт «Послуги» і перейти в розділ вибору потрібних йому послуг. Або може вибрати пункт «Особистий кабінет» і перейти в розділ роботи зі своїми персональними даними і сервісами. Або може вибрати розділ «Довідка». Поки все начебто просто і може бути представлено наступною діаграмою станів (максимально спрощеною):

Але давайте спробуємо зробити так, щоб за натисканні на кнопку «Скасувати» користувач міг повернутися з будь-якого розділу на стартову сторінку з вітальним повідомленням:

Схема ускладнюється, але все ще під контролем. Однак, давайте згадаємо, що в розділі «Послуги» у нас може бути ще декілька підрозділів, наприклад, «Популярні послуги», «Нові послуги» і «Повний перелік». І з кожного з цих розділів також треба повертатися на стартову сторінку. Наш простий КА стає все більш і більш непростим:

Але і це ще далеко не все. Адже ми поки ще не брали до уваги кнопку «Назад», за якою нам треба повернутись в попередній розділ. Давайте додамо ще й реакцію на кнопку «Назад» і подивимося, що у нас виходить:

Так, тепер ми бачимо шлях до справжнього веселощів. А адже ми ще навіть не розглядали підрозділи в розділах «Особистий кабінет» і «Довідка»… Якщо почнемо, то практично відразу ж наш простий, по початку, КА перетвориться у щось неймовірне.

Ось тут нам на допомогу і приходить вкладеність станів. Давайте уявимо, що у нас є всього два стани верхнього рівня: WelcomeScreen і UserSelection. Всі наші розділи (тобто «Послуги», «Особистий кабінет» і «Довідка») будуть «вкладені» в стан UserSelection. Можна сказати, що стану ServicesScreen, ProfileScreen і HelpScreen будуть дочірніми для UserSelection. А раз вони дочірні, то вони будуть успадковувати реакцію на якісь сигнали з батьківського стану. Тому реакцію на кнопку «Скасувати» ми можемо визначити в UserSelection. Але нам нема чого визначати цю реакцію у всіх дочірніх подсостояниях. Що робить наш КА більш лаконічним і зрозумілим:

Тут можна звернути увагу, що реакцію для «Скасування» і «Назад» ми визначили в UserSelection. І ця реакція на кнопку «Скасувати» працює для всіх без винятку подсостояний UserSelection (включаючи ще одне складне подсостояние ServicesSelection). Але ось в подсостоянии ServicesSelection реакція на кнопку «Назад» вже своя — повернення відбувається не в WelcomScreen, а в ServicesSelection.

КА, в яких використовується ієрархія/вкладеність станів, називаються ієрархічними кінцевими автоматами (ІКА).

Реакція на вхід/вихід в/із стану

Дуже корисною можливістю є можливість призначити реакцію на вхід в конкретний стан, а також реакцію на вихід із стану. Так, у наведеному вище прикладі з инфокиоском на вхід в кожне з станів можна повісити обробник, який буде змінювати вміст екрану інфокіоски.

Попередній приклад можна трохи розширити. Припустимо, в WelcomScreen у нас буде два подсостояния: BrightWelcomScreen, в якому екран буде підсвічений нормально, а також DarkWelcomScreen, в якому яскравість екрану буде зменшена. Ми можемо зробити обробник входу в DarkWelcomScreen, який буде зменшувати яскравість екрану. І обробник виходу з DarkWelcomScreen, який буде відновлювати нормальну яскравість.

Автоматична зміна стану по закінченню заданого часу

Часом буває необхідно обмежити час перебування КА в конкретному стані. Так, у наведеному вище прикладі ми можемо обмежити час перебування нашого ІКА в стані BrightWelcomScreen однією хвилиною. Як тільки минає хвилина, ІКА автоматично переводиться у стан DarkWelcomScreen.

Історія стану КА

Ще однією дуже корисною фичей ІКА є історія стану КА.

Давайте уявимо собі, що у нас є якийсь абстрактний ІКА ось такого вигляду:

Цей наш ІКА може переходити з TopLevelState1 в TopLevelState2 і назад. Але всередині TopLevelState1 є кілька вкладених станів. Якщо ІКА просто переходить з TopLevelState2 в TopLevelState1, то активуються відразу два стани: TopLevelState1 і NestedState1. NestedState1 активується тому, що це початкове подсостояние стану TopLevelState1.

Тепер уявімо, що далі наш ІКА змінював свій стан від NestedState1 до NestedState2. Всередині NestedState2 активувалося подсостояние InternalState1 (оскільки воно початкове для NestedState2). А з InternalState1 ми прешли в InternalState2. Таким чином, у нас одночасно активні наступні стани: TopLevelState1, NestedState2 і InternalState2. І тут ми переходимо в TopLevelState2 (тобто ми взагалі пішли з TopLevelState1).

Активним стає TopLevelState2. Після чого ми хочемо повернуться в TopLevelState1. Саме в TopLevelState1, а не в якесь конкретне подсостояние в TopLevelState1.

Отже, з TopLevelState2 ми йдемо в TopLevelState1 і куди ж ми потрапляємо?

Якщо у TopLevelState1 немає історії, то ми прийдемо в TopLevelState1 і NestedState1 (оскільки NestedState1 — це початкове подсостояние для TopLevelState1). Тобто вся історія про переходи всередині TopLevelState1, які здійснювалися до відходу в TopLevelState2, повністю загубилася.

Якщо ж у TopLevelState1 є т. н. поверхнева історія (shallow history), то при поверненні з TopLevelState2 в TopLevelState1 ми потрапляємо в NestedState2 і InternalState1. У NestedState2 ми потрапляємо тому, що це записано в історії стану TopLevelState1. А в InternalState1 ми потрапляємо тому, що воно є початковим для NestedState2. Виходить, що в поверхневої історії для TopLevelState1 зберігається інформація тільки про подсостояниях першого рівня. Історія вкладених станів у цих подсостояниях не зберігається.

Читайте також  Доказ наявності місць, де симетрії не можуть існувати

А ось якщо у TopLevelState1 є глибока історія (deep history), то при поверненні з TopLevelState2 в TopLevelState1 ми потрапляємо в NestedState2 і InternalState2. Тому, що в глибокій історії зберігається повна інформація про активних подсостояниях, незалежно від їх глибини.

Ортогональні стану

Досі ми розглядали ІКА у яких всередині стану могло бути активним лише одне з подсостояний. Але часом можуть бути ситуації, коли в конкретному стані ІКА має бути кілька одночасно активних подсостояния. Такі подсостояния називаються ортогональними станами.

Класичний приклад, на якому демонструють ортогональні стану — це звична нам клавіатура комп’ютера та її режими NumLock, CapsLock і ScrollLock. Можна сказати, що робота з NumLock/CapsLock/ScrollLock описується ортогональними подсостояниями всередині стану Active:

Все, що ви хотіли знати про кінцеві автомати, але…

Взагалі, є основоположна стаття на тему формальної нотації для діаграм станів від Девіда Харела: Statecharts: A Visual Formalism For Complex Systems (1987).

Там розбираються різні ситуації, які можуть зустрітися при роботі з кінцевими автоматами на прикладі управління звичайними електронними годинами. Якщо хтось не читав її, то дуже рекомендую. В принципі, все, що описував Харел потім перейшло в нотації UML. Але коли читаєш опис діаграм станів з UML-я, то не завжди розумієш що, для чого і коли потрібно. А от у статті Харела виклад йде від простих ситуацій до більш складним. І ти краще усвідомлюєш всю міць, яку приховують у собі кінцеві автомати.

Кінцеві автомати в SObjectizer
Далі ми будемо говорити про SObjectizer-е і його специфіки. Якщо вам не зовсім зрозумілі приклади, що наводяться нижче, то може мати сенс дізнатися про SObjectizer-е побільше. Наприклад, з нашої оглядової статті про SObjecizer і кількох наступних, які знайомлять читачів з SObjectizer-му переходячи від простого до складного (перша стаття, друга і третя).

Агенти в SObjectizer — це кінцеві автомати

Агенти в SObjectizer з самого початку були кінцевими автоматами з явно вираженими станами. Навіть якщо розробник агента не описував ніяких власних станів у своїй класі агента, все одно у агента було дефолтний стан, яке використовувалося за замовчуванням. Наприклад, якщо розробник зробив ось такого тривіального агента:

class simple_demo final : public so_5::agent_t {
public:
 // Сигнал для того, щоб агент надрукував на консоль свій статус.
 struct how_are_you final : public so_5::signal_t {};
 // Сигнал для того, щоб агент завершив свою роботу.
 struct quit final : public so_5::signal_t {};

 // Т. к. агент дуже простий, то робимо все підписки у конструкторі.
 simple_demo(context_t ctx) : so_5::agent_t{std::move(ctx)} {
so_subscribe_self()
 .event<how_are_you>([]{ std::cout << "i'm fine!" << std::endl; })
 .event<quit>([this]{ so_deregister_agent_coop_normally(); });
}
};

то він може навіть не підозрювати, що в реальності всі зроблені ним підписки зроблені для дефолтного стану. Але якщо розробник додає агенту свої власні статки, то вже доводиться замислюватися про те, щоб правильно підписати агента в правильному стані. Ось, скажімо, проста (і, як водиться) неправильна модифікація показаного вище агента:

class simple_demo final : public so_5::agent_t {
 // Стан, який означає, що агент вільний.
 state_t st_free{this};
 // Стан, який означає, що агент зайнятий.
 state_t st_busy{this};
public:
 // Сигнал для того, щоб агент надрукував на консоль свій статус.
 struct how_are_you final : public so_5::signal_t {};
 // Сигнал для того, щоб агент завершив свою роботу.
 struct quit final : public so_5::signal_t {};

 // Т. к. агент дуже простий, то робимо все підписки у конструкторі.
 simple_demo(context_t ctx) : so_5::agent_t{std::move(ctx)} {
so_subscribe_self()
 .event<quit>([this]{ so_deregister_agent_coop_normally(); });
 // На повідомлення how_are_you реагуємо по-різному, в залежності від стану.
 st_free.event([]{ std::cout << "i'm free" << std::endl; });
 st_busy.event([]{ std::cout << "i'm busy" << std::endl; });
 // Починаємо працювати в змозі st_free.
 this >>= st_free; 
}
};

Ми поставили два різних обробника сигналу how_are_you, кожен для свого стану.

А помилка у даній модифікації агента simple_demo в тому, що перебуваючи в st_free або st_busy агент не буде реагувати на quit взагалі, т. до. ми залишили підписку на quit у дефолтному стані, але не зробили відповідних підписок для st_free і st_busy. Простий і очевидний спосіб виправити цю проблему — це додати відповідні підписки у st_free і st_busy:

 simple_demo(context_t ctx) : so_5::agent_t{std::move(ctx)} {
 // На повідомлення how_are_you реагуємо по-різному, в залежності від стану.
st_free
 .event([]{ std::cout << "i'm free" << std::endl; })
 .event<quit>([this]{ so_deregister_agent_coop_normally(); });
st_busy
 .event([]{ std::cout << "i'm busy" << std::endl; })
 .event<quit>([this]{ so_deregister_agent_coop_normally(); });
 // Починаємо працювати в змозі st_free.
 this >>= st_free; 
 }

Правда, цей спосіб пахне копипастой, що не є добре. Від копипасты можна позбутися ввівши загальне батьківське стан для st_free і st_busy:

class simple_demo final : public so_5::agent_t {
 // Загальні батьківські стан для всіх подсостояний.
 state_t st_basic{this};
 // Стан, який означає, що агент вільний.
 // Є також початковим подсостоянием для st_basic.
 state_t st_free{initial_substate_of{st_basic}};
 // Стан, який означає, що агент зайнятий.
 // Є звичайним подсостоянием для st_basic.
 state_t st_busy{substate_of{st_basic}};
public:
 // Сигнал для того, щоб агент надрукував на консоль свій статус.
 struct how_are_you final : public so_5::signal_t {};
 // Сигнал для того, щоб агент завершив свою роботу.
 struct quit final : public so_5::signal_t {};

 // Т. к. агент дуже простий, то робимо все підписки у конструкторі.
 simple_demo(context_t ctx) : so_5::agent_t{std::move(ctx)} {
 // Обробник для quit визначаємо в st_basic і цей обробник
 / / "успадкований" вкладеними подсостояниями.
 st_basic.event<quit>([this]{ so_deregister_agent_coop_normally(); });
 // На повідомлення how_are_you реагуємо по-різному, в залежності від стану.
 st_free.event([]{ std::cout << "i'm free" << std::endl; });
 st_busy.event([]{ std::cout << "i'm busy" << std::endl; });
 // Починаємо працювати в змозі st_free.
 this >>= st_free; 
}
};

Заради справедливості треба додати, що спочатку в SObjectizer агенти могли бути тільки простими кінцевими автоматами. Підтримка ієрархічних КА з’явилася відносно недавно, в січні 2016-го року.

Чому в SObjectizer-е агенти є кінцевими автоматами?

У цього питання дуже проста відповідь: так вийшло коріння у SObjectizer-а зростають зі світу АСУТП, а там кінцеві автомати використовуються дуже часто. Тому ми вважали за потрібне, щоб агенти в SObjectizer також були кінцевими автоматами. Це дуже зручно якщо прикладної задачі, для якої SObjectizer намагаються застосувати, КА використовуються. А дефолтний стан, яке є у всіх агентів, що дозволяє не думати про КА, якщо використання КА не потрібно.

В принципі, якщо подивитися на саму Модель Акторів, та на принципи, на яких ця модель побудована:

  • актор – це якась сутність, що володіє поведінкою;
  • актори реагують на вхідні повідомлення;
  • отримавши повідомлення актор може:
    • надіслати деяку кількість повідомлень іншим акторам;
    • створити деяку кількість нових акторів;
    • визначити для себе нове поведінка для обробки наступних повідомлень.

То можна знайти сильне схожість між простими КА і акторами. Можна навіть сказати, що актори — це прості кінцеві автомати і є.

Читайте також  Кротові нори в JavaScript

Які можливості просунутих кінцевих автоматів SObjectizer підтримує?

З перерахованих вище можливостей просунутих кінцевих автоматів SObjectizer підтримує всі, за винятком ортогональних станів. Інші смаколики, начебто вкладених станів, обробників входу/виходу, обмежень на час перебування в стані, історії станів, підтримуються.

З підтримкою ортогональних станів з першого разу не зрослося. З одного боку, внутрішня архітектура SObjectizer-а не була призначена для підтримки декількох незалежних і одночасно активних станів у агента. З іншого боку, є ідеологічні питання про те, як повинна себе вести агент, у якого є ортогональні стану. Клубок цих питань виявився надто складним, а корисний вихлоп надто невеликим для того, щоб вирішити дану проблему. Так і в нашій практиці не зустрічалося поки ще ситуацій, де обов’язково потрібні ортогональні стану, але при цьому не можна було б обійтися, наприклад, розподілом роботи між декількома агентами, прив’язаними до одного спільного робочого контексту.

Однак, якщо комусь така фіча, як ортогональні стану, дійсно потрібна і у вас є реальні приклади задач, де це затребувано, то давайте поспілкуємося. Можливо, маючи конкретні приклади перед очима, ми зможемо додати в SObjectizer і цю фічу.

Як підтримка просунутих можливостей ІКА виглядає в коді

У цій частині розповіді ми спробуємо швиденько пробігтися по API SObjectizer-5 для роботи з ІКА. Без глибокого занурення в деталі, просто для того, щоб у читача виникло уявлення про те, що є і як це виглядає. Більш детальну інформації, буде таке бажання, можна розшукати в офіційній документації.

Вкладені стану

Для того, щоб оголосити вкладене стан, потрібно передати в конструктор відповідного об’єкта state_t вираз initial_substate_of або substate_of:

class demo : public so_5::agent_t {
 state_t st_parent{this}; // Батьківське стан.
 state_t st_first_child{initial_substate_of{st_parent}}; // Перше дочірнє подсостояние.
 // До того ж початкова.
 state_t st_second_child{substate_of{st_parent}}; // Друге дочірнє подстостояние.
 state_t st_third_child{substate_of{st_parent}}; // Третє дочірнє подсостояние.

 state_t st_first_grandchild{initial_substate_of{st_third_child}}; // Ще один рівень вкладеності.
 state_t st_second_grandchild{substate_of{st_third_child]};
...
};

Якщо у стану S є кілька подсостояний C1, C2, …, Cn, то одне з них (і тільки одне) має бути позначено як initial_substate_of. Порушення цього правила діагностується в run-time.

Максимальна глибина вкладеності станів у SObjectizer-5 обмежена. У версії 5.5 — це 16 рівнів. Порушення цього правила діагностується в run-time.

Найголовніший фокус з вкладеними станами в тому, що коли активується стан у якого є вкладені стану, то активується відразу кілька станів. Припустимо, є стан A, у якого є подсостояния B і C, а в подсостоянии B є подсостояния D і E:

Коли активується стан A, то, насправді, активується відразу три стану: A, A B і A. B. D.

Той факт, що може бути активно відразу кілька станів, чинить серйозний вплив на дві архіважливих речі. По-перше, на пошук обробника для чергового вхідного повідомлення. Так, у наведеному щойно прикладі обробник для повідомлення буде спершу шукатися в стані A. B. D. Якщо там відповідний обробник не буде знайдений, то пошук продовжиться в його батьківській стані, тобто в A. B. І вже зачеплений, якщо потрібно, пошук буде продовжено у стані A.

По-друге, наявність декількох активних станів позначається на порядку виклику обробників входу-виходу для станів. Але про це мова піде нижче.

Обробники входу-виходу для станів

Для стану можуть бути задані обробники входу, стану і виходу із стану. Робиться це за допомогою методів state_t::on_enter і state_t::on_exit. Зазвичай ці методи викликаються в методі so_define_agent() (або прямо в конструкторі агента, якщо агент тривіальний і спадкування від нього не передбачено).

class demo : public so_5::agent_t {
 state_t st_free{this};
 state_t st_busy{this};
...
 void so_define_agent() override {
 // Важливо: обробники входу і виходу визначаємо до того,
 // як стан агента буде змінено.
 st_free.on_enter([]{ ... });
 st_busy.on_exit([]{ ...});
...
 this >>= st_free;
}
...
};

Ймовірно, самий складний момент з on_enter/on_exit обробниками — це використання їх для вкладених капіталів. Давайте ще раз повернемося до прикладу з станами A, B, C, D і E.

Припустимо, що у кожного стану є on_enter і on_exit обробник.

Нехай поточним станом агента стає A. тобто активуються стану A, A B і A. B. D. В процесі зміни стану агента будуть викликані: A. on_enter, A. B. on_enter і A. B. D. on_enter. І саме в такому порядку.

Припустимо, потім відбувається перехід в A. B. E. Будуть викликані: A. B. D. on_exit і A. B. E. on_enter.

Якщо ми переведемо агента в стан A. C, то будуть викликані A. B. E. on_exit, A. B. on_exit, A. C. on_enter.

Якщо агент, перебуваючи в стані A. C буде дерегистрирован, то відразу після завершення методу so_evt_finish() будуть викликані обробники A. C. on_exit і A. on_exit.

Ліміти часу

Ліміт часу на перебування агента в конкретному стані задається за допомогою методу state_t::time_limit. Як і у випадку з on_enter/on_exit, методи time_limit зазвичай викликаються там, де агент налаштовується для своєї роботи всередині SObjectizer:

class led_indicator : public so_5::agent_t {
 state_t inactive{this};
 state_t active{this};
...
 void so_define_agent() override {
 // Дозволяємо знаходиться в цьому стані не більш 15s.
 // Після закінчення заданого часу потрібно перейти в inactive.
 active.time_limit(15s, inactive);
...
}
...
};

Якщо ліміт часу для стану заданий, то як тільки агент входить в це стан, SObjectizer починає відлік часу перебування в стані. Якщо ж агент залишає стан, а потім повертається в цей стан знову, то відлік часу починається заново.

Якщо ліміти часу задаються для вкладених станів, то потрібно бути уважним, оскільки можливі цікаві фокуси:

class demo : public so_5::agent_t {
 // Стану верхнього рівня.
 state_t A{this}, B{this};
 // Вкладені в first стану.
 state_t C{initial_substate_of{A}}, st_D{substate_of{A}};
...
 void so_define_agent() override {
 A. time_limit(15s, B);
 C. time_limit(10s, D);
 D. time_limit(20s, C);
...
}
...
};

Припустимо, агент входить в стан A. тобто активуються стану A і C. І для A, і для C починається відлік часу. Раніше він закінчиться для стану C і агент перейде в стан D. При цьому почнеться відлік часу для перебування у стані D. Але продовжиться відлік часу перебування в A! Оскільки при переході з C D агент продовжив залишатися в стані A. І через п’ять секунд після примусового переходу з C D агент піде в стан B.

Історія для стану

За замовчуванням стану агента не мають історію. Щоб активувати збереження історії для стану потрібно передати в конструктор state_t константу shallow_history (у стану поверхнева історія) або deep_history (у стан буде зберігатися глибока історія). Наприклад:

class demo : public so_5::agent_t {
 state_t A{this, shallow_history};
 state_t B{this, deep_history};
...
};

Історія станів — це непроста тема, особливо коли використовується пристойна глибина вкладеності станів і у подсостояний є своя історія. Тому за більш повною інформацією на цю тему краще звернутися до документації, поекспериментувати. Ну і розпитати нас, якщо самостійно розібратися не виходить 😉

Читайте також  Розширення для хрому: створення, публікація, досвід

just_switch_to, transfer_to_state, suppress

У класу state_t є ряд найбільш часто використовуваних методів, які вже були показані вище: event() для підписки події на повідомлення, on_enter() і on_exit() для встановлення обробників входу-виходу, time_limit() завдання ліміту на час перебування в змозі.

Поряд з цими методами при роботі з ІКА дуже корисними виявляються наступні методи класу state_t:

Метод just_switch_to(), який призначений для випадку, коли єдиною реакцій на вхідне повідомлення є переклад агента в новий стан. Можна написати:

some_state.just_switch_to<some_msg>(another_state);

замість:

some_state.event([this](mhood_t<some_msg>) {
 this >>= another_state;
});

Метод transfer_to_state() виявляється дуже корисним, коли у нас деякий повідомлення M обробляється однаковим чином в двох або більше стани S1, S2, …, Sn. Але, якщо ми перебуваємо в стані S2,…,Sn, то нам спершу доводиться повернутися в S1, а вже потім робити обробку M.

Якщо це звучить дивно, то, можливо, у прикладі коду ця ситуація буде зрозуміла краще:

class demo : public so_5::agent_t {
 state_t S1{this}, S2{this}, ..., Sn{this};
...
 void actual_M_handler(mhood_t<M> cmd) {...}
...
 void so_define_agent() override {
S1.event(&demo::actual_M_handler);
...
 // У всіх інших станах ми повинні спершу перевести агента в S1,
 // а вже потім делегувати обробку M реальному обробникові.
 S2.event([this](mhood_t<M> cmd) {
 this >>= S1;
actual_M_handler(cmd);
});
 ... // Так і для всіх інших станів.
 Sn.event([this](mhood_t<M> cmd) {
 this >>= S1;
actual_M_handler(cmd);
});
}
...
};

От замість того, щоб для S2,…,Sn визначати дуже схожі обробники подій можна використовувати transfer_to_state:

class demo : public so_5::agent_t {
 state_t S1{this}, S2{this}, ..., Sn{this};
...
 void actual_M_handler(mhood_t<M> cmd) {...}
...
 void so_define_agent() override {
S1.event(&demo::actual_M_handler);
...
 // У всіх інших станах ми повинні спершу перевести агента в S1,
 // а вже потім делегувати обробку M реальному обробникові.
S2.transfer_to_state<M>(S1);
 ... // Так і для всіх інших станів.
Sn.transfer_to_state<M>(Sn);
}
...
};

Метод suppress() пригнічує пошук обробника події для поточного подсостояния і всіх його батьківських подсостояний. Нехай у нас є батьківські стан A, в якому на повідомлення M викликається std::abort(). І є дочірнє стан B, в якому M можна безпечно проігнорувати. Ми повинні визначити реакцію на M в подсостоянии B, адже якщо ми цього не зробимо, то обробник для B буде знайдений A. Тому нам потрібно буде написати щось на зразок:

void so_define_agent() override {
 A. event([](mhood_t<M>) { std::abort(); });
...
 B. event([](mhood_t<M>) {}); // Самі нічого не робимо, але й не дозволяємо шукати
 // обробник для M в батьківських станах.
...
}

Метод suppress() дозволяє записати цю ситуацію в коді більш явно і наочно:

void so_define_agent() override {
 A. event([](mhood_t<M>) { std::abort(); });
...
 B. suppress<M>(); // Самі нічого не робимо, але й не дозволяємо шукати
 // обробник для M в батьківських станах.
...
}

Дуже простий приклад

До складу штатних прикладів SObjectizer v.5.5 входить простий приклад blinking_led, який імітує роботу миготливого світлодіодного індикатора. Діаграма станів агента з цього прикладу виглядає наступним чином:

А ось як виглядає повний код агента з цього прикладу:

class blinking_led final : public so_5::agent_t
{
 state_t off{ this }, blinking{ this },
 blink_on{ initial_substate_of{ blinking } },
 blink_off{ substate_of{ blinking } };

public :
 struct turn_on_off : public so_5::signal_t {};

 blinking_led( context_t ctx ) : so_5::agent_t{ ctx }
{
 this >>= off;

 off.just_switch_to< turn_on_off >( blinking );

 blinking.just_switch_to< turn_on_off >( off );

blink_on
 .on_enter( []{ std::cout << "ON" << std::endl; } )
 .on_exit( []{ std::cout << "off" << std::endl; } )
 .time_limit( std::chrono::milliseconds{1250}, blink_off );

blink_off
 .time_limit( std::chrono::milliseconds{750}, blink_on );
}
};

Тут вся фактична робота виконується всередині обробників входу-виходу для подсостояния blink_on. Ну і, плюс до того, працюють ліміти на час перебування в подсостояниях blink_on і blink_off.

Не дуже простий приклад

До складу штатних прикладів SObjectizer v.5.5 входить також набагато більш складний приклад, intercom_statechart, який імітує поведінку панелі домофона. І діаграма станів головного агента у цьому прикладі виглядає приблизно так:

Все так суворо тому, що ця імітація підтримує не тільки виклик квартири за номером, але і такі штуки, як унікальний секретний код для кожної квартири, а так само особи сервісний код. Ці коди дозволяють відкрити замок двері нікуди не додзвонюючись.

У даному прикладі є ще цікаві штуки. Але він занадто великий, щоб розписувати його в деталях (на це навіть окремої статті може бути мало). Так що якщо цікаво, як в SObjectizer-е виглядають дійсно складні ІКА, то можна подивитися в цьому прикладі. А якщо щось буде незрозуміло, то можна поставити запитання нам. Наприклад, у коментарях до даної статті.

Чи можна не використовувати вбудовану в SObjectizer-5 підтримку КА?
Отже, в SObjectizer-5 є вбудована підтримка ІКА з досить широким набором підтримуваних функцій. Зроблена ця підтримка, ясна річ, для того, щоб цим користувалися. Зокрема, налагоджувальні механізми SObjectizer-а, начебто message delivery tracing-а, знають про стан агента і відображають поточний стан у своїх відповідних налагоджувальних повідомленнях.

Тим не менш, якщо розробник не хоче з якихось причин використовувати вбудовані засоби SObjectizer-5, то він може цього не робити.

Наприклад, відмовитися від застосування SObjectizer-івських state_t і іже з ними можна із-за того, що state_t — це досить таки важкий об’єкт, у якого всередині std::string, пара std::function, кілька лічильників типу std::size_t, п’ять покажчиків на різні об’єкти і ще якась дрібниця. Все разом це на 64-х бітовому Linux-е і GCC-5.5, наприклад, дає 160 байт на один state_t (не рахуючи того, що може бути розміщено в динамічної пам’яті).

Якщо вам в додатку потрібно, скажімо, мільйон агентів, у кожного з яких буде по 10 станів, то накладні витрати на SObjectizer-івські state_t можуть бути неприйнятними. В цьому випадку можна використовувати який-небудь інший механізм роботи з кінцевими автоматами, вручну делегуючи обробку повідомлень цього механізму. Щось на зразок:

class external_fsm_demo : public so_5::agent_t {
 some_fsm_type my_fsm_;
...
 void so_define_agent() override {
so_subscribe_self()
 .event([this](mhood_t<msg_one> cmd) { my_fsm_.handle(*cmd); })
 .event([this](mhood_t<msg_two> cmd) { my_fsm_.handle(*cmd); })
 .event([this](mhood_t<msg_three> cmd) { my_fsm_.handle(*cmd); });
...
}
...
};

У цьому випадку ви розраховуєтесь за ефективність збільшенням кількості ручної роботи і відсутністю допомоги з боку налагодження механізмів SObjectizer-а. Але тут вже розробнику вирішувати.

Висновок
Стаття вийшло об’ємною, набагато більше, ніж планувалося спочатку. Спасибі всім, хто дочитав до цього місця. Якщо хто-небудь з читачів вважатиме можливим залишити свій фідбек у коментарях до статті, то це буде чудово.

Якщо щось залишилося незрозуміло, то задавайте питання, ми з задоволенням відповімо.

Також користуючись нагодою, хочеться звернути увагу тих, хто цікавиться SObjectizer-му, що почалася робота над наступною версією SObjectizer-а в рамках гілки 5.5. Коротко про те, що розглядається до реалізації в 5.5.23, описано тут. Більш повно, але англійською, тут. Ви можете залишити свою думку про будь-який з пропонованих до реалізації фіч, або запропонувати щось ще. Тобто є реальна можливість вплинути на розвиток SObjectizer-а. Тим більше, що після релізу v.5.5.23 можлива пауза в роботах над SObjectizer-му і наступної можливості включити до складу SObjectizer-а що-небудь корисного в 2018-му може і не бути.

Степан Лютий

Обожнюю технології в сучасному світі. Хоча частенько і замислююся над тим, як далеко вони нас заведуть. Не те, щоб я прям і знаюся на ядрах, пікселях, коллайдерах і інших парсеках. Просто приходжу в захват від того, що може в творчому пориві вигадати людський розум.

You may also like...

Залишити відповідь

Ваша e-mail адреса не оприлюднюватиметься. Обов’язкові поля позначені *