Что такое Конечный Автомат?

Конечный Автомат (State Machine), также называемый Automata (да, как и игра), - это концепция для разработки, организации рабочих и технологических процессов с учетом текущего «состояния» какой-то задачи, изменения её состояний и, по возможности, для автоматизации процесса.

Так же хочу заметить в разнице между английским и русским названием: State Machine - дословно машина состояний, то есть система которая, выполняя действия, переходит от одного состояния к другому. Конечный Автомат - это устройство что выполняет какие-то автоматизированные действия и число возможных внутренних состояний которого конечно.

Я объясню на примере. Предположим, что я хочу купить молоко, тогда такая задача будет иметь примерно следующие состояния:

  • Начальное состояние

  • Поездка в магазин

  • Взятие молока

  • Произведение оплаты за молоко

  • Поездка обратно домой

Где это используется ?

Везде. В большинстве (если не во всех) бизнес-процессах, требующих как минимум двух «состояний». Например: службы доставки, операции купли-продажи, процесс найма и т.д.

Внимание! если вы новичок или опыт с ООП мал тогда не воспринимайте статью как руководство к действию. Конечные Автоматы не популярны коммерческой разработке и могут быть полезны в проектах, которые направлены на транзакции, например платёжные шлюзы. Задумываются о них лишь когда запутанность транзакций превышает все мыслимые пределы.

Зачем мне нужен Конечный Автомат?

Вернемся назад к примеру с молоком. Зачем мне нужно создавать конечный автомат, в котором процесс идет от А до Я?. Загвоздка в том что всегда что-то случается, могут возникнуть ошибки, недоразумения или проблемы.

Пример, в случае покупки молока: что делать, если я забуду деньги, или магазин закрыт, или все молоко разобрали, или если возникнут проблемы с вождением (гололёд, снегопад) ?

Таким образом состояния для нас теперь следующие:

  • Начальное состояние

  • Поездка в магазин

  • Отмена поездки

  • Взятие молока

  • Произведение оплаты за молоко

  • Невозможность покупки

  • Поездка обратно домой

Можно даже добавить больше состояний, но для нашего исследования тут хватит основных. Также возможно сжатие состояний и использование меньшего их количества, например "состояние отмены поездки" и "состояние отмены покупки молока" можно объединить просто в «Отмена».

Как автоматизировать процесс?

А теперь, как автоматизировать процесс, а точнее, как я могу автоматизировать изменение состояний? Каждое изменение состояния называется ПЕРЕХОДОМ. Переход возможен при изменении значений переменных, результатов функции/методов или истечения/наступления времени - это все система использует для автоматизации. На примере молока я буду использовать следующие значения (поля):

  • milk = 0 нет молока, milk = 1 у меня есть молоко

  • money = кол-во денег в кармане

  • price = цена молока

  • stock_milk = количество молока в магазине

  • store_open = 0 магазин закрыт, = 1 открыт

  • gas = бензин моей машины

Так мы можем сформировать состояния и переходы:

Начальное состояние

Состояние после перехода

Когда возможен переход?

Исходное состояние

Поездка за молоком

Когда milk = 0 и gas > 0

Поездка в магазин

Отмена поездки (это конец процесса)

Когда gas = 0

Поездка в магазин

Взятие молока

store_open = 1 и stock_milk > 0

Поездка в магазин

Невозможность покупки

store_open = 0 или stock_milk = 0

Взятие молока

Произведение оплаты за молоко

(это также увеличивает milk +1)

money >= price

Взятие молока

Невозможность покупки

money < price

Невозможность покупки

Поездка обратно домой (конец процесса)

всегда

Плата за молоко

Поездка обратно домой (конец процесса)

всегда

Код PHP

В нашем примере (меньше 100 строк кода) мы опускаем некоторые ключевые части проекта. Например, если мы захотим использовать робота, который будет выполнять всю работу от и до, то ему потребуются специальные датчики и груда программного обеспечения с которым нужно будет интегрироваться - так вот этого всего делать мы не будем :)

Теперь давайте запрограммируем это.

Во-первых, используя Composer скачаем нужную для нашего примера библиотеку (она бесплатна для личных и коммерческих проектов):

composer require eftec/statemachineone

Библиотека поддерживает различные типы подключений к базам данных. В этом примере мы будем использовать MySQL и коннектор MySQLi.

Создадим новый проект: нам нужно запрограммировать только один файл.

Часть первая

Сначала мы инициализировали код, добавили библиотеку и создали новый экземпляр StateMachineOne();

<?php

use eftec\statemachineone\StateMachineOne;

require __DIR__ . "/vendor/autoload.php";

$sMachine = new StateMachineOne(null);
$sMachine->setDebug(true);

Вторая, наши переменные

Теперь мы определяем наши переменные, такие как состояния и начальные значения.

<?php
// состояния специфичны для данного проекта
const INITIAL_STATE = 1;
const DRIVING_TO_BUY_MILK = 2;
const CANCEL_DRIVING = 3;
const PICKING_THE_MILK = 4;
const PAYING_FOR_THE_MILK = 5;
const UNABLE_TO_PURCHASE = 6;
const DRIVE_BACK_HOME = 7;
$sMachine->setDefaultInitState(INITIAL_STATE);

$sMachine->setStates([
    INITIAL_STATE => 'Начальное состояние',
    DRIVING_TO_BUY_MILK => 'Поездка в магазин',
    CANCEL_DRIVING => 'Отмена поездки',
    PICKING_THE_MILK => 'Взятие молока',
    PAYING_FOR_THE_MILK => 'Произведение оплаты за молоко',
    UNABLE_TO_PURCHASE => 'Невозможность покупки',
    DRIVE_BACK_HOME => 'Поездка обратно домой',
]);

$sMachine->fieldDefault = [
    'milk' => 0,
    'money' => 9999,
    'price' => 10,
    'stock_milk' => 80,
    'store_open' => true,
    'gas' => 10,
];

Итак, используем ли мы те же состояния, определенные концептуально? Да и это важно. Очень легко пропустить шаг или важную операцию (такое может привести к порче всей цепочки переходов), поэтому код должен быть как можно более чистым.

Третья, подключение к базе данных

Теперь мы подключились к базе данных (MySQL). Мы должны установить базу данных (в примере база установлена на localhost), пользователя (root), пароль (root) и базу данных/схему для использования (statemachinedb). Библиотека использует базу данных как дополнительный компонент. Автоматически будут созданы две таблицы: buymilk_jobs и buymilk_logs (можно создать и вручную если хочется).

<?php
// Параметры базы данных
$sMachine->tableJobs = "buymilk_jobs";
$sMachine->tableJobLogs = "buymilk_logs"; // опционально
$sMachine->setDB('mysql', 'localhost', 'root', 'root', 'my_automata_php');
$sMachine->createDbTable(false); // true - создавать новую таблицу при каждом запуске.

$sMachine->loadDBAllJob(); // Загружаем все задачи, в том числе готовые.
//$sMachine->loadDBActiveJobs(); // для использования на проде. Загружает все задания из БД со всеми активными состояниями

Четвертая, определение переходов

Теперь мы определяем переходы между состояниями.

<?php
// Бизнес правила
$sMachine->addTransition(INITIAL_STATE, DRIVING_TO_BUY_MILK, 'when milk = 0 and gas > 0');
$sMachine->addTransition(INITIAL_STATE, CANCEL_DRIVING, 'when gas = 0', 'stop');
$sMachine->addTransition(DRIVING_TO_BUY_MILK, PICKING_THE_MILK, 'when store_open = 1 and stock_milk > 0');
$sMachine->addTransition(DRIVING_TO_BUY_MILK, UNABLE_TO_PURCHASE, 'when store_open = 0 or stock_milk = 0');
$sMachine->addTransition(PICKING_THE_MILK, PAYING_FOR_THE_MILK, 'when money >= price set milk = 1');
$sMachine->addTransition(PICKING_THE_MILK, UNABLE_TO_PURCHASE, 'when money < price');
$sMachine->addTransition(UNABLE_TO_PURCHASE, DRIVE_BACK_HOME, 'when timeout', 'stop');
$sMachine->addTransition(PAYING_FOR_THE_MILK, DRIVE_BACK_HOME, 'when timeout', 'stop');

Что означает строка «when…». ? Это язык конечного автомата, он базовый, но работу свою делает. Однако для каждой команды требуется место. Также обратите внимание на пример: money <price неверно, а money < price (видим пробелы) правильно.

Пятая, и, наконец, мы объединим все это вместе.

В чем заключается магия Автоматов - в методе checkAllJobs(). Кроме того, у библиотеки есть визуальный интерфейс, чтобы мы могли тестировать задания. Этот UI не является обязательным, т.к. каждое задание может быть запрограммировано с помощью кода.

<?php
$msg = $sMachine->fetchUI();// читает пользовательский ввод из UI и возвращает вспомогательное сообщение (это необязательно и нужно только для отладки).
$sMachine->checkAllJobs(); // проверяем все доступные задачи

$sMachine->viewUI(null, $msg); // null означает что будем отлаживать текущую задачу

Тестирование с использованием пользовательского интерфейса

Если конфигурация верна, то на странице должен отображаться новый экран.

Теперь давайте создадим новую задачу. Давайте начнем с начальными значениями:milk = 0, money = 999 и gas = 10 и нажмем на кнопку "Create a new job". Теперь задача была создана и перешла из состояния "Вождение автомобиля"(1) в состояние " Купить молоко"(2).

Job #11 2018–12–09 19:06:11.232900 [INFO]: state changed from 1 to 2 changed (it is a log message thanks to the debug option)

Давайте установим следующие значения, store_open=1 и stock_milk=1 и нажмем на кнопку Set field values (она установит новые значения и проверит все условия).

Таким образом, работа перескочила с Поездка в магазин (2) -> Взятие молока (4), и она устанавливает значение молока равным 1.

Кроме того, работа перескочила с Взятие молока (4) -> Оплата молока (5) на том же шаге. Почему? Это потому, что действие соответствует ее условиям перехода.

Job #11 2018–12–09 19:09:06.502700 [INFO]: state changed from 2 to 4 changed setting milk = 1
Job #11 2018–12–09 19:09:06.514600 [INFO]: state changed from 4 to 5 changed

Теперь давайте нажмем кнопку «Refresh» (она снова проверит все задания), и задача изменит состояние с «Произведение оплаты за молоко» (5) -> «Поездка обратно домой» (7), и задача остановится, завершив цикл.

Job #11 2018–12–09 19:12:41.435900 [INFO]: state changed from 5 to 7 stopped

Замечания и предложения

Мы ознакомились с теорией Конечных Автоматов и немного применили на практике. Используемая библиотека содержит ошибки (пришлось напрячься чтобы запустить её) но для академического интереса вполне подходит. Данная статья является переводом, так что если найдете ошибки - пишите.

Комментарии (11)


  1. b00b1ik
    09.08.2021 22:56
    +1

    Отмена поездки — как долго вы находитесь в этом состоянии?)))

    Каша из состояний и событий, которые переводят одно состояние в другое, у вас и в коде и голове))


  1. dopusteam
    10.08.2021 00:44

    'when milk = 0 and gas > 0'

    Писать такое в 2021 - где то за гранью. Разве в php не завезли анонимные функции какие нибудь?


    1. Djeux
      10.08.2021 08:57
      +2

      Завезли уже давно, но тут видимо дело в написании фрагментов "человекочитаемых для непрограммистов".


  1. xEpozZ
    10.08.2021 09:21
    +1

    Вы серьезно думаете, что Стейт машина должна ходить в бд и читать консоль?

    Как на счёт отправки смс и вебсокетов? :)


  1. BetsuNo
    10.08.2021 09:21
    +2

    Прекратите писать примеры из 90-х. Любой новичок, набредший на эту статью, воспримет это как руководство к действию. Уже вышла 8-я версия языка, но до сих пор во многом благодаря подобным статьям пишут так, как будто последних двадцати лет не было. Неужели сложно было обернуть это дело в класс, избавив глобальную область видимости от мусора? Если вы думаете, что так понятнее, то не спешите с выводами. Если человек не может в азы ООП, то и конечные автоматы ему пока не нужны.

    Также нам понадобится база данных MySQL и коннектор MySQLi. Это стандартные инструменты PHP.

    Почему mysqli? Кто определил этот стандарт? MySQL не является инструментом PHP, это просто популярная СУБД. Вместо mysqli лучше использовать PDO, как более универсальное решение. Кстати, библиотека, предложенная вами, использует собственную обёртку над PDO, которая поддерживает MS SQL, MySQL и Oracle. И в этом, как мне видится, есть некоторая слабость этой библиотеки, т.к. много где используют PostgreSQL, и подозреваю, что именно в контексте PHP побольше, чем MS SQL и Oracle вместе взятые.


    1. Dionisvl Автор
      10.08.2021 09:39
      -1

      Спасибо, добавил пункт для новичков и ООП, MySQL тоже поправил.

      Вам рекомендую посмотреть интересный доклад о целенаправленном переходе с ООП на конечные автоматы в Мегафоне, ссылка приведена в статье.


      1. BetsuNo
        10.08.2021 13:04

        Что, простите? Переход с ООП на конечные автоматы? Послушал по ссылке кусок про автоматы, там ни слова о подобной глупости, наоборот там упоминаются абстракции. ООП - парадигма в программировании, при которой программа состоит из объектов, описываемых классами. Также существуют другие парадигмы, подробнее можно посмотреть тут. Конечный автомат - алгоритм, который можно реализовать, используя разные парадигмы. Кстати, используемая в статье библиотека реализована с использованием ООП.


    1. sergyx
      10.08.2021 13:10
      +1

      Прекратите переживать за новичков.
      Прекратите считать новичков идиотами и расписываться за их всех.
      Прекратите заставлять авторов статей перегружать свой код ненужными абстракциями только потому что вам так хочется. Ах здесь засоряется глобальное пространсво - давайте впихнём сюда Симфони. А вы уверены, что здесь лучше подойдёт MySql, а не PostgreSQL?


      1. dopusteam
        10.08.2021 14:46

        Ну в текущем виде код точно не несёт пользы

        Откуда взялись плюсы к посту - для меня вообще загадка


  1. tas
    10.08.2021 10:20
    +1

    Описать события в таблице недостаточно - в табличном виде крайне тяжело найти ошибки и потерянные переходы. А вот если их нарисовать, тогда все становится намного проще и понятнее.

    Так что диаграмма - это один из best practices при разработке конечных автоматов.


  1. Solpadoin
    10.08.2021 14:40

    Пример не очень удачный. Кстати, конечный автомат хорошо бы зашёл в проектах, которые направлены на транзакции. Те же платёжные шлюзы. Где выполняется последовательность команд как одна сущность - транзакция. Если что-то пошло не так - откат назад до начала этой сукупности команд. Ну и последовательная операция со статусами что б невозможно было перепрыгнуть с шага на шаг, только вперёд либо закончить сразу всё.