Что такое Конечный Автомат?
Конечный Автомат (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
Замечания и предложения
Мы ознакомились с теорией Конечных Автоматов и немного применили на практике. Используемая библиотека содержит ошибки (пришлось напрячься чтобы запустить её) но для академического интереса вполне подходит. Данная статья является переводом, так что если найдете ошибки - пишите.