Начинает он с одного выпадающего окошка, тестирует анимацию, выставляет 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. Вот как-то так:
- наследуемся от класса FiniteStateMachine,
- создаем методы с названием 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/
Добавить комментарий