Создаем Конечный Автомат на PHP

Kate

Administrator
Команда форума

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

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

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

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

aa1964f378a0a98f93e9af3daff06056.jpeg

  • Начальное состояние
  • Поездка в магазин
  • Взятие молока
  • Произведение оплаты за молоко
  • Поездка обратно домой

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

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

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

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

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

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

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

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

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

  • 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. Это стандартные инструменты PHP.

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

Часть первая​

Сначала мы инициализировали код, добавили библиотеку и создали новый экземпляр 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 означает что будем отлаживать текущую задачу

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

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

f5fb758234562b2a18e39fe654c47b12.png

Теперь давайте создадим новую задачу. Давайте начнем с начальными значениями: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

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


 
Сверху