Простые стейт-машины на службе у разработчика

от автора

Представьте на минутку обычного программиста. Допустим, его зовут Вася и ему нужно сделать анимированную менюшку на сайт/десктоп приложение/мобильный апп. Знаете, которые выезжают сверху вниз, как меню у окна Windows или меню с яблочком у OS X. Вот такое.

Начинает он с одного выпадающего окошка, тестирует анимацию, выставляет ease out 100% и наслаждается полученным результатом. Но вскоре он понимает, что для того, чтобы управлять менюшкой, хорошо бы знать закрыто оно сейчас или нет. Мы-то с вами тут программисты опытные, все понимаем, что нужно добавить флаг. Не вопрос, флаг есть.

var opened = false;

Вроде, работает. Но, если быстро кликать по кнопке, меню начинает моргать, открываясь и закрываясь не успев доанимироваться в конечное состояние. Вася добавляет флаг animating. Теперь код у нас такой:

var opened = false; var animating = false;  function onClick(event) {   if (animating) return;   if (opened) close();   else open(); } 

Через какое-то время Васе говорят, что меню может быть полностью выключено и неактивно. Не вопрос! Мы-то с вами тут программисты опытные, все понимаем, что… нужно добавить ЕЩЕ ОДИН ФЛАГ! И, всего-то через пару дней разработки, код меню уже пестрит двустрочными IF-ами типа вот такого:

if (enabled && opened && !animating && !selected && finishedTransition && !endOfTheWorld && ...) { ... }

Вася начинает задаваться вопросами: как вообще может быть, что animating == true и enabled == false; почему у него время от времени все глючит; как тут вообще поймешь в каком состоянии находится меню. Ага! Состояния… О них дальше и пойдет речь.

Знакомьтесь, это Вася.


Состояние

Вася уже начинает понимать, что многие комбинации флагов не имеют смысла, а остальные можно легко описать парой слов, например: Disabled, Idle, Animating, Opened. Все мы тут программисты опытные, сразу вспоминаем про state machines. Но, для Васи придется рассказать что это и зачем. Простым языком, без всяких математических терминов.

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

Из схемы понятно, что из состояния Inactive в Active можно попасть только по событию Begin, а из состояния Paused можно попасть как и в Active, так и в Inactive. Такую простую концепцию почему-то называют «Конечный Автомат» или «Finite State Machine», что очень пугает обычных людей.

По завету ООП, состояния должны быть скрыты внутри объекта и просто так снаружи не доступны. Например, у объекта во время работы может быть 20 разных состояний, но внешнее API на вопрос «чо как дела?» отвечает «ничо так» на 19 из них и только на 1 ругается матом, что проср*ли все полимеры.

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

Самая простая в мире стейт машина

Допустим, теперь Вася делает проект на C# и ему нужна простая стейт машина для одного типа объектов. Он пишет что-то типа такого:

private enum State { Disabled, Idle, Animating }  private State state;   void setState(State value) {     state = value;     switch (state) {         case State.Disabled:             ...         case State.Idle:             ...         case State.Animating :             ...         break;     } } 

А вот так обрабатывает события в зависимости от текущего состояния:

void event1Handler() {     switch (state) {         case State.Idle:             ...         break;     } } 

Но, мы-то с вами тут программисты опытные, все понимаем, что метод setState в итоге разрастется на пару десятков страниц, что (как написано в учебниках) не есть хорошо.

State Pattern

Погуглив пару часов, Вася решает, что State Pattern идеально подходит в данной ситуации. Тем более, что старшие программисты все время соревнуются кто больше паттернов запихнет в свой апп, так что, решает Вася, паттерны это дело важное.

Например, для State Pattern можно сделать интерфейс IState:

public interface IState {     void Event1();     void Event2(); } 

И по отдельному классу для каждого состояния, которые этот интерфейс имплементят. В теории выглядит красиво и 100% по учебнику.

Но, во-первых, для каждой несчастной мелкой стейт машины нужно городить уйму классов, что само по себе небыстро. Во-вторых, рано или поздно начнутся проблемы с доступом к общим данным. Где их хранить? В основном классе? А как классы-состояния получат к ним доступ? А как мне тут за 15 минут перед дедлайном впилить быстро мелкий хак в обход правил? И подобные проблемы взаимодействия, которые будут сильно тормозить разработку.

Реализация на основе особенностей языка

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

  1. наследуемся от класса FiniteStateMachine,
  2. создаем методы с названием stateName_eventName(), которые автоматически вызываются при переходе по состояниям и при обработке событий

Лишнего кода писать действительно приходится сильно меньше.

Реализовав систему описанную выше, Вася понимает, что у нее тоже больше минусов, чем плюсов:

  • Нужно наследоваться от класса FiniteStateMachine,
  • В реакциях на кастомные события также нужно писать большие switch конструкции,
  • Нет возможности передать параметры при изменении состояния.

Фреймворк

А тем временем, Вася уже вовсю стал вникать в теорию стейт машин и решил, что хорошо бы иметь возможность формально их описывать через API или (о Боже) через XML, что в теории звучит круто. Мы-то с вами тут программисты опытные, все понимаем, что нужно писать свой фреймворк. Потому что другие не подходят, так как у всех у них есть один фатальный недостаток.

Вася решил, что с помощью его фреймворка можно будет быстро и легко создать стейт машину без необходимости писать много ненужного кода. Фреймворк не будет накладывать никаких ограничений на разработчика. Все вокруг будут веселы и жизнерадостны.

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

Вот, например, описание конечного автомата фреймворком stateless:

var phoneCall = new StateMachine<State, Trigger>(State.OffHook);  phoneCall.Configure(State.OffHook)     .Permit(Trigger.CallDialed, State.Ringing);          phoneCall.Configure(State.Ringing)     .Permit(Trigger.HungUp, State.OffHook)     .Permit(Trigger.CallConnected, State.Connected);   phoneCall.Configure(State.Connected)     .OnEntry(() => StartCallTimer())     .OnExit(() => StopCallTimer())     .Permit(Trigger.LeftMessage, State.OffHook)     .Permit(Trigger.HungUp, State.OffHook)     .Permit(Trigger.PlacedOnHold, State.OnHold); 

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

XML

XML — это отдельное зло. Кто-то когда-то придумал использовать его для написания конфигов. Стадо леммингов java разработчиков длительное время молилось на него. А теперь никто уже и не знает зачем все используют XML, но продолжают бить всех, кто пытается от него избавиться.

Вася тоже загорелся идеей, что можно все сконфигурировать в XML и НЕ ПИСАТЬ НИ СТРОЧКИ КОДА! В итоге в его фреймворке отдельно лежат XML файлы примерно такого содержания:

<fsm name="Vending Machine">     <states>         <state name="start">             <transition input="nickel" next="five" />             <transition input="dime" next="ten" />             <transition input="quarter" next="start" action="dispense" />         </state>         <state name="five">             <transition input="nickel" next="ten" />             <transition input="dime" next="fifteen" />             <transition input="quarter" next="start" action="dispense" />         </state>         <state name="ten">             <transition input="nickel" next="fifteen" />             <transition input="dime" next="twenty" />             <transition input="quarter" next="start" action="dispense" />         </state>         ...     </states> </fsm>

Класс! И никакого программирования. Но, мы-то с вами тут программисты опытные, все понимаем, что программирование никуда не ушло. Вася заменил кусок императивного кода на кусок декларативного кода, добавив при этом во фреймворк интерпретатор XML, который все еще в пару раз усложнил. А потом попробуй это отдебажить, когда код на разных языках и разбросан по проекту.

Соглашение

И тут Васе все это надоело и он вернулся обратно к самому простому в мире конечному автомату. Он его немного переделал и придумал правила как писать в нем код. Получилось следующее:

/** * Названия состояний описываются enum или строковыми константами, если язык не поддерживает enums. */  private enum State { Disabled, Idle, Animating }   /** * Текущее состояние всегда скрыто. Иногда, бывает полезно добавить еще и переменную с предыдущим состоянием. */  private State state;   /** * Все смены состояний происходят только через вызов методов state<название состояния>(). * В них сперва может быть выполнена логика для выхода из предыдущего состояния. * После чего выполняется setState(newValue) и специфическая для состояния логика. */  void stateDisabled() {     switch (state) {         case State.Idle:         break;     }     setState(State.Disabled);     // State Disabled enter logic }  /** * У функций смены состояний могут быть параметры. * stateIdle(0); */ void stateIdle(int data) {     setState(State.Idle);     // State Idle enter logic }   void stateAnimating() {     setState(State.Animating);     // State Animating enter logic }   /** * Обычно setState состоит только из * state = value; * или еще prevState = state; если нужно хранить предыдущее состояние. * Но, также здесь может быть общая логика для выхода из предыдущего состояния. */ void setState(State value) {     switch ( state ) {         case State.Animating:             // state Animating exit logic         break;         // other states     }     state = value; }  /** * Обработчики событий делают только то, что можно в текущем состоянии. */   void event1Handler() {     switch (state) {         case State.Idle:             // state Idle logic         break;         // other states     } } 

Простое соглашение для написания стейт машин. Оно достаточно гибкое и имеет следующие плюсы:

  • почти вся логика при смене состояний находится в методах stateA(), что позволяет разбить гигантский switch в setState() и сделать код более читаемым,
  • смена состояния происходит только через методы stateA(), что облегчает отладку,
  • новому состоянию легко можно передавать параметры, например, если у книги есть состояние Page, то перейти на новую страницу можно просто сменив состояния вызвав statePage(42)
  • в обработчиках событий всегда понятно какая логика выполняется в каких состояниях,
  • все члены команды знают где писать логику для входа и выхода из состояния,
  • нет необходимости в каком-то фреймворке и предварительной конфигурации конечного автомата,
  • есть возможность грязно все похачить в последний момент, если уж совсем подругому никак.

Еще одним неочевидным плюсом является независимость соглашения от языка. Перейдя с одной платформы на другую, не придется переписывать свой любимый фреймворк на другой язык или искать ему достойную замену.

Заключение

На этом заканчивается увлекательное приключение Васи в мире стейт машин. А ведь впереди еще столько всего интересного. Отдельного топика бы только заслужили параллельные и зависимые стейт машины.

Я надеюсь, что, если вы еще не используете стейт машины повсеместно, эта статья перетянет вас на сторону добра; если вы пишите свой уберфреймворк для работы со стейт машинами, она поможет свежим взглядом посмотреть на то, что у вас получается.

Я надеюсь, что эта статья поможет разработчикам задуматься где и когда стоит использовать паттерны и фреймворки, и что описанное соглашение по оформлению стейт машин окажется кому-то полезным.

ссылка на оригинал статьи http://habrahabr.ru/post/160105/