(495) 925-0049, ITShop интернет-магазин 229-0436, Учебный Центр 925-0049
  Главная страница Карта сайта Контакты
Поиск
Вход
Регистрация
Рассылки сайта
 
 
 
 
 

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

Источник: habrahabr
valyard

Представьте на минутку обычного программиста. Допустим, его зовут Вася и ему нужно сделать анимированную менюшку на сайт/десктоп приложение/мобильный апп. Знаете, которые выезжают сверху вниз, как меню у окна 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; почему у него время от времени все глючит; как тут вообще поймешь в каком состоянии находится меню. Ага! Состояния... О них дальше и пойдет речь.

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



Состояние

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

Соглашение

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

UPDATE:  спасибо за комментарии. Здесь действительно не хватало небольшого объяснения.

У нас есть несколько состояний. Переход между ними - это транзакция из атомарных операций, то есть они все происходят всегда вместе, в правильном порядке и между ними не может вклиниться еще какой-то код. При смене состояния с A на B происходит следующее: выполняется код выхода из состояния A, состояние меняется с A на B, выполняется код входа в состояние B.

Для перехода на состояние A нужно вызвать метод stateA, который выполнит нужную логику и вызовет setState(A). Самому вызывать setState(A) крайне не рекомендуется.

Получилось следующее:

/**
* Названия состояний описываются 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
    }
}

UPDATE: В setState() пишется уникальная логика выхода из состояния, а в stateB() возможна специфическая логика выхода из состояния A при переходе в B. Но очень редко используется. 

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

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

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

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

UPDATE:  а setState() вполне можно заменить одним сеттером для наглядности.

Заключение

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

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

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

Ссылки по теме


 Распечатать »
 Правила публикации »
  Написать редактору 
 Рекомендовать » Дата публикации: 27.11.2012 
 

Магазин программного обеспечения   WWW.ITSHOP.RU
VMware Horizon Apps Standard, v7 : 10 Pack (Named User)
VMware Fusion 10 Pro, ESD
Allround Automation Direct Oracle Access Standard license
IBM Domino Messaging Client Access License Authorized User License + SW Subscription & Support 12 Months
SAP Crystal Reports XI R2 Dev 2006 INTL WIN NUL License (Version 11)
 
Другие предложения...
 
Курсы обучения   WWW.ITSHOP.RU
 
Другие предложения...
 
Магазин сертификационных экзаменов   WWW.ITSHOP.RU
 
Другие предложения...
 
3D Принтеры | 3D Печать   WWW.ITSHOP.RU
 
Другие предложения...
 
Новости по теме
 
Рассылки Subscribe.ru
Информационные технологии: CASE, RAD, ERP, OLAP
Новости ITShop.ru - ПО, книги, документация, курсы обучения
СУБД Oracle "с нуля"
OS Linux для начинающих. Новости + статьи + обзоры + ссылки
Новые материалы
Вопросы и ответы по MS SQL Server
Программирование на Visual Basic/Visual Studio и ASP/ASP.NET
 
Статьи по теме
 
Новинки каталога Download
 
Исходники
 
Документация
 
 



    
rambler's top100 Rambler's Top100