State машина что это

Конечный автомат: теория и реализация

Авторизуйтесь

Конечный автомат: теория и реализация

State машина что это. finite automata. State машина что это фото. State машина что это-finite automata. картинка State машина что это. картинка finite automata

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

Мы уже публиковали серию статей по написанию искусственного интеллекта при помощи конечного автомата. Если вы еще не читали эту серию, то можете сделать это сейчас:

Примечание автора Хоть в статье используются ActionScript 3 и Flash, вы с легкостью можете писать на удобном для вас языке.

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

Конечный автомат (или попросту FSM — Finite-state machine) это модель вычислений, основанная на гипотетической машине состояний. В один момент времени только одно состояние может быть активным. Следовательно, для выполнения каких-либо действий машина должна менять свое состояние.

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

State машина что это. fsm enemy brain. State машина что это фото. State машина что это-fsm enemy brain. картинка State машина что это. картинка fsm enemy brain

Описание состояний автомата

Конечный автомат можно представить в виде графа, вершины которого являются состояниями, а ребра — переходы между ними. Каждое ребро имеет метку, информирующую о том, когда должен произойти переход. Например, на изображении выше видно, что автомат сменит состояние «wander» на состояние «attack» при условии, что игрок находится рядом.

Планирование состояний и их переходов

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

State машина что это. fsm ant brain. State машина что это фото. State машина что это-fsm ant brain. картинка State машина что это. картинка fsm ant brain

Описание состояний интеллекта муравья

Отправной точкой является состояние «find leaf», которое остается активным до тех пор, пока муравей не найдет лист. Когда это произойдет, то состояние сменится на «go home». Это же состояние останется активным, пока наш муравей не доберется до муравейника. После этого состояние вновь меняется на «find leaf».

Если состояние «find leaf» активно, но курсор мыши находится рядом с муравьем, то состояние меняется на «run away». Как только муравей будет в достаточно безопасном расстоянии от курсора мыши, состояние вновь сменится на «find leaf».

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

State машина что это. fsm ant brain no transition. State машина что это фото. State машина что это-fsm ant brain no transition. картинка State машина что это. картинка fsm ant brain no transition

Описание состояний интеллекта муравья. Обратите внимание на отсутствие перехода между «run away» и «go home»

Реализация простого конечного автомата

Конечный автомат можно реализовать при помощи одного класса. Назовем его FSM. Идея состоит в том, чтобы реализовать каждое состояние как метод или функцию. Также будем использовать свойство activeState для определения активного состояния.

Всякое состояние есть функция. Причем такая, что она будет вызываться при каждом обновлении кадра игры. Как уже говорилось, в activeState будет храниться указатель на функцию активного состояния.

Метод update() класса FSM должен вызываться каждый кадр игры. А он, в свою очередь, будет вызывать функцию того состояния, которое в данный момент является активным.

Метод setState() будет задавать новое активное состояние. Более того, каждая функция, определяющая какое-то состояние автомата, не обязательно должна принадлежать классу FSM — это делает наш класс более универсальным.

Использование конечного автомата

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

State машина что это. fsm ant brain code. State машина что это фото. State машина что это-fsm ant brain code. картинка State машина что это. картинка fsm ant brain code

Описание состояний интеллекта муравья, сосредоточенное на коде

Наш муравей представлен классом Ant, в котором есть поле brain. Это как раз экземпляр класса FSM.

Класс Ant также содержит свойства velocity и position. Эти переменные будут использоваться для расчета движения с помощью метода Эйлера. Функция update() вызывается при каждом обновлении кадра игры.

Для понимания кода мы опустим реализацию метода moveBasedOnVelocity(). Если хотите узнать поподробнее на тему движения, прочитайте серию статей Understanding Steering Behaviors.

Ниже приводится реализация каждого из методов, начиная с findLeaf() — состояния, ответственного за поиск листьев.

Состояние goHome() — используется для того, чтобы муравей отправился домой.

И, наконец, состояние runAway() — используется при уворачивании от курсора мыши.

Улучшение FSM: автомат, основанный на стеке

Представьте себе, что муравью на пути домой также нужно убегать от курсора мыши. Вот так будут выглядеть состояния FSM:

State машина что это. fsm ant brain all transitions. State машина что это фото. State машина что это-fsm ant brain all transitions. картинка State машина что это. картинка fsm ant brain all transitions

Обновленное описание состояний интеллекта муравья

Кажется, что изменение тривиальное. Нет, такое изменение создает нам проблему. Представьте, что текущее состояние это «run away». Если курсор мыши отдаляется от муравья, что он должен делать: идти домой или искать лист?

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

State машина что это. fsm stack. State машина что это фото. State машина что это-fsm stack. картинка State машина что это. картинка fsm stack

Конечный автомат, основанный на стеке

А вот и наглядная демонстрация работы конечного автомата, основанного на стеке:

State машина что это. fsm stack possible scenes. State машина что это фото. State машина что это-fsm stack possible scenes. картинка State машина что это. картинка fsm stack possible scenes

Переходы в FSM, основанном на стеке

Реализация FSM, основанного на стеке

Такой конечный автомат может быть реализован так же, как и простой. Отличием будет использование массива указателей на необходимые состояния. Свойство activeState нам уже не понадобится, т.к. вершина стека уже будет указывать на активное состояние.

Обратите внимание, что метод setState() был заменен на pushState() (добавление нового состояния в вершину стека) и popState() (удаление состояния на вершине стека).

Использование FSM, основанного на стеке

Важно отметить, что при использовании конечного автомата на основе стека каждое состояние несет ответственность за свое удаление из стека при отсутствии необходимости в нем. Например, состояние attack() само должно удалять себя из стека в том случае, если враг был уже уничтожен.

Вывод

Конечные автоматы, безусловно, полезны для реализации логики искусственного интеллекта в играх. Они могут быть легко представлены в виде графа, что позволяет разработчику увидеть все возможные варианты.

Реализация конечного автомата с функциями-состояниями является простым, но в то же время мощным методом. Даже более сложные переплетения состояний могут быть реализованы при помощи FSM.

Источник

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

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

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

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

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

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

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

State машина что это. e26f2ac1ff10bc14ff62f731067f358c. State машина что это фото. State машина что это-e26f2ac1ff10bc14ff62f731067f358c. картинка State машина что это. картинка e26f2ac1ff10bc14ff62f731067f358c

Состояние

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

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

State машина что это. image loader. State машина что это фото. State машина что это-image loader. картинка State машина что это. картинка image loader

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

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

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

State машина что это. b4229e7f97a83be08c5fe445c86d581c. State машина что это фото. State машина что это-b4229e7f97a83be08c5fe445c86d581c. картинка State машина что это. картинка b4229e7f97a83be08c5fe445c86d581c

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

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

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

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

State Pattern

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

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

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

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

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

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

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

Фреймворк

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

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

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

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

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

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

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

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

Соглашение

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

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

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

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

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

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

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

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

Заключение

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

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

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

Источник

Стейт-машины на службе у MVP. Лекция Яндекса

Модель конечного автомата (finite-state machine, FSM) находит применение в написании кода для самых разных платформ, включая Android. Она позволяет сделать код менее громоздким, неплохо укладывается в парадигму Model-View-Presenter (MVP) и поддаётся несложному тестированию. Разработчик Владислав Кузнецов рассказал на Droid Party, как эта модель помогает в развитии приложения Яндекс.Диск.

— Вначале поговорим по теорию. Думаю, каждый из вас слышал и про MVP, и про стейт-машину, но повторим.

State машина что это. . State машина что это фото. State машина что это-. картинка State машина что это. картинка

Поговорим о мотивации, о том, зачем это все нужно и как это нам может помочь. Перейдем к тому, что у нас получилось, на реальном примере покажу кусочки кода. И в конце поговорим о тестировании, о том, как этот подход помог удобно все тестировать.

Стейт-машину и MVP или что-то похожее — наверное, MVI — использовали все.

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

State машина что это. . State машина что это фото. State машина что это-. картинка State машина что это. картинка

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

State машина что это. . State машина что это фото. State машина что это-. картинка State машина что это. картинка

Область применения стейт-машины довольно большая. По каждому пункту они используются и успешно применяются.

State машина что это. 6kz6mxlq mdtra5x duehczdoro. State машина что это фото. State машина что это-6kz6mxlq mdtra5x duehczdoro. картинка State машина что это. картинка 6kz6mxlq mdtra5x duehczdoro

Как любой подход, MVP разделяет наше приложение на несколько слоев. View — чаще всего Activity или Fragment, задача которой пробросить какое-то действие пользователю, идентифицировать Presenter о том, что пользователь что-то сделал. Model мы рассматриваем как поставщик данных. Это может быть как БД, если мы говорим про clean architecture, или Interactor, что угодно может быть. И Presenter — посредник, который связывает View и модель, при этом может у модели что-то забрать и обновить View. Этого нам достаточно.

Кто может в одном предложении сказать, что такое программа? Исполняемый код? Слишком общо, нужно детальнее. Алгоритм? Алгоритм — это последовательность действий.

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

State машина что это. nwzjck2q097uwbqk89wzm98pe7e. State машина что это фото. State машина что это-nwzjck2q097uwbqk89wzm98pe7e. картинка State машина что это. картинка nwzjck2q097uwbqk89wzm98pe7e

Представьте простой класс, у которого три каких-то булевых флага. Чтобы гарантировать, что вы покрыли все сценарии сочетания этих флагов, вам надо 2³ сценариев. Надо гарантированно покрыть восемь сценариев, чтобы сказать, что я точно все сочетания флагов обрабатываю. Если вы добавляете еще один флаг, это пропорционально увеличивается.

Мы столкнулись с похожей проблемой. Вроде была простая задача, но по мере разработки и работы над ней мы стали понимать, что что-то идет не так. Я буду рассказывать на примере фичи, которую мы запустили. Она называется удаление локальных фото. Смысл в том, что пользователь отгружает какие-то данные в облако в автоматическом режиме. Скорее всего, это фото и видео, которые он снял на свой телефон. Получается, что файлы вроде есть в облаке. Зачем занимать драгоценное место на телефоне, когда можно эти фоточки удалить?

State машина что это. 3n3plhy4 4mu3v2m4p4gkygdkky. State машина что это фото. State машина что это-3n3plhy4 4mu3v2m4p4gkygdkky. картинка State машина что это. картинка 3n3plhy4 4mu3v2m4p4gkygdkky

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

State машина что это. . State машина что это фото. State машина что это-. картинка State машина что это. картинка

Мы посмотрели — вроде ничего сложного нет. Диалог, две TextView, чек-бокс, кнопки. Но когда мы начали детально работать над этой проблемой — поняли, что получить данные о том, сколько файлов мы можем удалить, это задача достаточно долговременная. Поэтому пользователю мы должны показать некую заглушку. Это псевдокод, в реальной жизни он выглядит по-другому, но смысл такой же.

State машина что это. . State машина что это фото. State машина что это-. картинка State машина что это. картинка

Мы проверяем какое-то состояние, проверяем, что у нас идут вычисления, и рисуем заглушечку «Подождите».

State машина что это. v5zl. State машина что это фото. State машина что это-v5zl. картинка State машина что это. картинка v5zl

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

State машина что это. . State машина что это фото. State машина что это-. картинка State машина что это. картинка

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

State машина что это. . State машина что это фото. State машина что это-. картинка State машина что это. картинка
State машина что это. . State машина что это фото. State машина что это-. картинка State машина что это. картинка

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

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

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

И самое печальное — какое-то непредвиденное поведение, которое на этапе тестирования пропустили, там ничего не случилось, а где-то в продакшене у пользователя в лучшем случае ничего не случилось, какой-то кривой UI, а в худшем — упало или данные потеряли. Просто приложение повело себя не консистентно.

Как же решать эту проблему? При помощи силы стейт-машины.

State машина что это. 7tj. State машина что это фото. State машина что это-7tj. картинка State машина что это. картинка 7tj

Основная задача, с которой справляется стейт-машина, это берет большую сложную задачу и разбивает на маленькие дискретные состояния, с которыми проще взаимодействовать и управлять. Посидев, подумав, так как мы стараемся делать что-то MVP, как прикрутить ко всему этому наше состояние? Мы пришли примерно к такой схеме. Кто читал книгу GOF, это классический паттерн-стейт, просто то, что там называется контекстом, я обозвал стейт-оунером, и по факту это презентер. Презентер обладает этим состоянием, умеет их переключать, и еще может предоставлять какие-то данные нашим состояниям, если они хотят что-то знать, например, размер файлов или хотят запросить асинхронный запрос, выбрать.

State машина что это. . State машина что это фото. State машина что это-. картинка State машина что это. картинка

Ничего тут суперского нет, скорее важен следующий слайд.

State машина что это. . State машина что это фото. State машина что это-. картинка State машина что это. картинка

С этого надо начинать разработку, когда вы начинаете делать стейт-машину. Вы садитесь за свой компьютер или где-то за столом, и либо на бумажке, либо в специальных инструментах рисуете диаграмму состояний. Тоже ничего сложного нет, но у этого этапа очень много плюсов. Во-первых, на раннем этапе вы можете сразу задетектить какие-то неконсистентности в бизнес-логике. Ваши продакты могут прийти, изъявить свое желание, вроде все хорошо, но когда вы беретесь писать код, вы понимаете, что что-то не стыкуется. Думаю, с каждым была такая ситуация. Но когда вы делаете диаграммку, вы на раннем этапе можете увидеть, что что-то не стыкуется. Рисуется она достаточно просто, есть специальные инструменты типа PlantUML, в которых даже рисовать не надо уметь, надо уметь писать псевдокод, и она сама генерит графики.

Примерно так выглядит наша диаграммка, которая описывает состояние этого диалога. Тут несколько состояний и логика перехода между ними.

State машина что это. ec84hlidyk6 olcmjrar9s2oiq0. State машина что это фото. State машина что это-ec84hlidyk6 olcmjrar9s2oiq0. картинка State машина что это. картинка ec84hlidyk6 olcmjrar9s2oiq0

Перейдем к коду. Сам State, тут ничего важного, главное то, что у него три метода: onEnter, который при входе вызывает в первую очередь invalidateView. Для чего это сделано? Чтобы как только мы приходим в состояние, обновился UI. Плюс есть метод invalidateView, который мы перегружаем, если надо что-то с UI сделать, и метод onExit, в котором можем что-то сделать, если выходим из состояния.

State машина что это. zold5sqpcjeu 1z3mq48c4jnos4. State машина что это фото. State машина что это-zold5sqpcjeu 1z3mq48c4jnos4. картинка State машина что это. картинка zold5sqpcjeu 1z3mq48c4jnos4

StateOwner. Интерфейс, который предоставляет возможность перещелкивания состояний. Как мы выяснили, это будет будущий презентер. И это методы, которые дают дополнительный доступ к данным. Если какие-то данные шарятся между состояниями, мы можем их держать в презентере, и через этот интерфейс отдавать. В данном случае мы можем отдать размер файлов, которые мы можем очистить, и предоставляем возможность сделать какой-то запрос. Мы перешли в состояние, хотим что-то запросить и через StateOwner можем позвать метод.

Еще такая полезность, что он тоже может вернуть ссылку на вьюшку. Это сделано для того, что если у вас состояние и прилетают какие-то данные, вы не хотите переходить в новое состояние, просто это избыточно, вы можете напрямую обновить вьюшку, текст. У нас это используется для того, чтобы обновлять количество цифр, которые пользователь видит, когда смотрит на диалог. У нас в runtime идет загрузка файлов, он смотрит на диалог, и цифры обновляются. Мы не переходим в новое состояние, мы просто обновляем текущую View.

State машина что это. . State машина что это фото. State машина что это-. картинка State машина что это. картинка

Тут стандартное MVP, все должно быть предельно простым, никакой логике, простые методы, которые что-то рисуют. Я придерживаюсь этой концепции. Не должно быть никакой логики, по минимуму каких-то действий. Чисто берем какой-то Text View, меняем его, не более.

State машина что это. 8h fzfwagetflmtss66is8shzly. State машина что это фото. State машина что это-8h fzfwagetflmtss66is8shzly. картинка State машина что это. картинка 8h fzfwagetflmtss66is8shzly

Presenter. Тут побольше интересных вещей. В первую очередь мы можем данные шарить через него для каких-то состояний, у нас есть две переменные, помеченные аннотацией State. Кто использовал Icepick, знаком с ней. Мы не пишем руками сериализацию в Partible, используем готовую библиотеку.

Следующее — начальное состояние. Всегда полезно задать начальное состояние, даже если оно ничего не делает. Полезность в том, что не нужно делать проверки на null, а если говорить о том, что оно может что-то делать. Например, вам нужно один раз что-то сделать за жизненный цикл вашего приложения, когда мы стартуем, надо один раз процедуру выполнить, и больше никогда ее не делать. При выходе из состояния initial мы всегда можем что-то такое выполнить, и мы никогда в это состояние не возвращаемся. Типа так нарисована диаграмма состояний. Хотя кто знает, кто нарисует, может, у вас можно будет вернуться.

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

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

State машина что это. rka21mszygspy9cjuprcqgv9afi. State машина что это фото. State машина что это-rka21mszygspy9cjuprcqgv9afi. картинка State машина что это. картинка rka21mszygspy9cjuprcqgv9afi

Пробросить надо saveState, если кто-то работал с подобными библиотеками, все довольно тривиально. Можете руками писать. И два метода очень важных: attach, вызываемый в onStart, и detach, вызываемый в onStop.

State машина что это. . State машина что это фото. State машина что это-. картинка State машина что это. картинка

В чем их важность? Изначально мы планировали аттачиться и детачиться в onCreateView, onDestroyView, но этого оказалось не совсем достаточно. Если у вас View, у вас может обновляться текст, а может показаться диалог-фрагмент. И если вы не задетачитесь в onStop, и потом попытаетесь показать фрагмент, вы словите всем известное исключение про то, что нельзя коммитить транзакцию, когда у нас сохранилось состояние. Либо используйте commit state loss, либо не делайте так. Поэтому мы детачимся в onStop, при этом там презентер продолжит работать, переключать состояния, отлавливать события. И в тот момент, когда случится старт, мы вызовем событие view attached, и презентер обновит UI в соответствие текущему состоянию.

State машина что это. . State машина что это фото. State машина что это-. картинка State машина что это. картинка
State машина что это. mysbt8ecs5ap56d5o jyndzgp a. State машина что это фото. State машина что это-mysbt8ecs5ap56d5o jyndzgp a. картинка State машина что это. картинка mysbt8ecs5ap56d5o jyndzgp a

Есть метод release, вызывается обычно в onDestroy, делаете детач и дополнительно освобождаете ресурсы.

State машина что это. f9lg. State машина что это фото. State машина что это-f9lg. картинка State машина что это. картинка f9lg

Еще важный метод setState. Так как мы в onEnter и onExit планируем изменять UI, то тут есть проверка на главный поток. Это создает нам ограничение, что мы здесь не делаем ничего тяжеловесного, все запросы должны быть либо к UI, либо должны быть асинхронными. Плюсом этого места является то, что здесь мы можем залогировать вход и выход из состояния, очень полезно бывает при отладке, например, когда что-то идет не так, вы можете посмотреть, как система перещелкивалась и понять, что же было не так.

State машина что это. . State машина что это фото. State машина что это-. картинка State машина что это. картинка

Пара примеров состояний. Есть состояние Initial, просто триггерит вычисления, сколько места нужно освободить в тот момент, когда вьюшка стала доступна. Это произойдет после onStart. Как только случается onStart, мы переходим в новое состояние, и система начинает запрашивать данные.

State машина что это. ntcyi3chfp0yvgyiauig5gegy2c. State машина что это фото. State машина что это-ntcyi3chfp0yvgyiauig5gegy2c. картинка State машина что это. картинка ntcyi3chfp0yvgyiauig5gegy2c

State машина что это. f4bqj kaebdsnml dpa azvgrmm. State машина что это фото. State машина что это-f4bqj kaebdsnml dpa azvgrmm. картинка State машина что это. картинка f4bqj kaebdsnml dpa azvgrmm

Пример состояния Calculating, мы у stateOwner реквестим размер файлов, он как-то лезет в базу, и потом еще есть inValidateView, мы обновляем текущий UI пользователя. И viewAttached вызовется в том случае, если вьюшка заново зааттачится. Если мы были в фоне, Calculating был в фоне, мы опять возвращаемся в нашу Activity, вызовется этот метод и актуализирует все данные.

State машина что это. eufg5fb rwg6keerfqik skfo44. State машина что это фото. State машина что это-eufg5fb rwg6keerfqik skfo44. картинка State машина что это. картинка eufg5fb rwg6keerfqik skfo44

Пример события, мы запросили у stateOwner сколько файлов можно освободить, и он вызывает метод filesSizeUpdated. Тут я поленился, можно было написать три отдельных метода, типа обновилось, старых файлов столько-то, как разные события разделить. Но надо понимать, когда-то у вас это сложно будет, когда-то гораздо проще. Не надо в оверинжениринг впадать, что каждое событие — отдельный метод. Можно вполне обойтись простым if, ничего страшного я в этом не вижу.

State машина что это. r0getxszg4i3mwlruqqybl mffm. State машина что это фото. State машина что это-r0getxszg4i3mwlruqqybl mffm. картинка State машина что это. картинка r0getxszg4i3mwlruqqybl mffm

Я вижу несколько потенциальных улучшений. Мне не нравится, что мы вынуждены пробрасывать руками эти методы, типа onStart, on Stop, onCreate, onSave и прочее. Можно привязаться к Lifecycle, но непонятно, как быть с saveState. Есть идея, например, сделать презентер фрагментом. Почему нет? Фрагмент без UI, который ловит жизненный цикл, и вообще нам тогда ничего не надо будет, все само будет к нам прилетать.

Еще интересный момент: этот презентер каждый раз пересоздается, и если у вас в презентере хранятся большие данные, вы в БД сходили, держите огромный курсор, то это недопустимо каждый раз запрашивать при повороте экрана. Поэтому можно кэшировать презентер, как это делает, например, ViewModule из Architecture Components, сделать какой-то фрагмент, который будет держать в себе кэш презентеров и возвращать их для каждой вьюшки.

Можно использовать табличный способ задания стейт-машин, потому что у тот паттерн стейт, который мы используем, обладает одним существенным минусом: как только вам нужно добавить один метод в новое событие, вы должны добавить имплементацию во все наследники. Хотя бы пустую. Либо сделать это в базовом состоянии. Это не очень удобно. Так что табличный способ задания стейт-машин используется во всех библиотеках — если поискать на GitHub по слову FSM, вы найдете большое количество библиотек, которые предоставляют вам некий билдер, где вы задаете начальное состояние, ивент и конечное состояние. Расширять и поддерживать такую стейт-машину гораздо проще.

Еще один интересный момент: если вы используете паттерн стейт, если у вас стейт-машина начинает разрастаться, скорее всего, вам придется какие-то ивенты хэндлить одинаково, чтобы код не копипастить, вы создаете базовое состояние. Чем больше ивентов, тем больше начинает появляться базовых состояний, иерархия разрастается, и что-то идет не так.

Как мы знаем, наследование надо заменять делегированием, и иерархические стейт-машины помогают такую проблему решить. У вас появляются состояния, которые зависят не от уровня наследования — просто строится дерево состояний, которые передают обработчик выше. Тоже можете почитать отдельно, очень полезная штука. В Android, например, иерархические стейт-машины используются в WatchDog Wi-Fi, который мониторит состояние сети, там они есть, прямо в исходниках Android.

State машина что это. kdqncnx u10ovabuexg h. State машина что это фото. State машина что это-kdqncnx u10ovabuexg h. картинка State машина что это. картинка kdqncnx u10ovabuexg h

Последний, но не менее важный момент. Как это можно тестировать? В первую очередь, можно тестировать детерминированные состояния. Есть отдельное состояние, мы создаем экземпляр, дергаем метод onEnter и смотрим, что у вьюшки вызвались соответствующие значения. Таким образом валидируем, что наше состояние правильно обновляет View. Если у вас View ничего серьезного не делает, то, скорее всего, вы покроете огромное количество сценариев.

State машина что это. bcmno pvrehzh447tidyzn3nd5w. State машина что это фото. State машина что это-bcmno pvrehzh447tidyzn3nd5w. картинка State машина что это. картинка bcmno pvrehzh447tidyzn3nd5w

Вы можете замокать какие-то методы функцией, которая возвращает размер, позвать после onEnter еще какое-то событие и посмотреть, как конкретное состояние реагирует на конкретные события. В данном случае при возникновении события filesSizeUpdated и когда AllFilesSize больше нуля, мы должны перейти в новое состояние CleanAllFiles. При помощи макета мы все это проверяем.

State машина что это. . State машина что это фото. State машина что это-. картинка State машина что это. картинка

И последнее — мы можем тестировать систему целиком. Мы конструируем состояние, отправляем в него ивент и проверяем, как система себя ведет. У нас есть три этапа тестирования. Отдельно тестируем, как обновляется UI, отдельно тестируем, как происходит логика перехода, переключения между состояниями, и отдельно тестируем всю систему в целом.

Мы переписывали видеоплеер на такую концепцию, и получили покрытие больше 70%. Под 80% инструкций было покрыто такими тестами. Мне кажется, это очень крутой показатель.

State машина что это. . State машина что это фото. State машина что это-. картинка State машина что это. картинка

Что мы получили, используя такую концепцию? В первую очередь — тестирование. Стейт-машину и наш презентер несложно подружить с жизненным циклом.

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

Порог вхождения в понятие стейт-машины достаточно низкий, и люди, которые приходят в команду, видят отдельные детерминированные состояния. Они могут легко понять, что они делают, и вариантов ошибиться гораздо меньше. Плюс за счет того, что у нас есть единая точка переключения состояний, мы можем это все логировать. Разные библиотеки для стейт-машин позволяют не только понять, какое состояние было старым, а какое новым. Они также позволяют вам, например, отлавливать определенные переходы. Вы можете поставить триггер lock на определенный переход. Если вы что-то отлаживаете, это может быть очень важно.

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

Источник

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *