Как стать автором
Обновить
189.84
hh.ru
HR Digital

Стейт-машины в iOS

Время на прочтение6 мин
Количество просмотров5.1K

Бизнес-логика – это сложно. Сложная бизнес-логика — ещё сложнее. А описать всё это в коде – просто жесть. Мы с вами каждый день реализуем тонну разных сценариев с огромным количеством веток развития. Каждую ветку нужно запрограммировать, потом суметь быстро поправить, а когда придёт продакт, еще и поменять ее логику. И если писать код просто как он пишется, можно оказаться в ситуации, когда простой фикс вместо 20 минут занимает 6 дней. Это проблема. 

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

Deus ex state-machina

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

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

В дикой природе существует множество типов стейт-машин, я не стану углубляться в их академическое описание или четкие UML-нотации. Сегодня я хочу поговорить про практическое применение, а для этого буду использовать максимально упрощенные диаграммы состояний. Если кому-то будет интересно копнуть поглубже, то можно для начала поискать информацию об автоматах Мили и Мура – просто погуглите “Finite-state machine”, "Конечный автомат" или черканите в наш чат в телеге. Кстати пишите в комментариях, если хотите отдельную статью про все это. 

Типичная стейт-машина представляет собой систему, в которую мы отправляем управляющие сигналы. Она их обрабатывает, и на выходе мы получаем новое состояние – тот набор данных, который мы храним продолжительное время. Кроме того, при необходимости стейт-машина может отправлять исходящий сигнал.

Представьте себе автомат для парковки. При выезде, в паркомате вы вводите номер машины и система предлагает оплатить за парковку 300 рублей. Вы вставляете 500 рублей и система для вашей машины меняет состояние на “Оплачено”. Вы можете свободно уезжать. Но вы заплатили больше, чем надо и система отправит сигнал, выдать сдачу в 200 рублей. Сдача не имеет никакого отношения к системе парковки и может не храниться в стейте, но это будет событие. 

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

Это очень удобно, потому что нам надо описать только один “правильный” сценарий работы с системой. А “неправильными” автоматически становятся все остальные, их мы можем обрабатывать все вместе. Когнитивная нагрузка снижается, нам становится проще работать – профит! 

А сейчас самое интересное. Рассмотрим конкретный пример, который хорошо иллюстрирует как можно выстроить работу со стейтами.

Стейт-машина в деле: проектирование

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

Разберемся, что происходит во время звонка. Никакого системного API для работы со звонками не существует. Есть просто URL ссылка с номером телефона –  и всё. Именно такую ссылку мы открываем при нажатии на номер телефона в приложении. После открытия этой ссылки система запрашивает подтверждение. И тут важно понимать, что алерт, который мы видим на экране не принадлежит нашему приложению. В момент его появления срабатывает системное событие willResignActive. 

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

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

Следующим шагом я выделил события жизненного цикла приложения. Это наш единственный способ отследить действия системы.

Еще нам понадобятся два наших собственных события. Одно, чтобы отслеживать факт начала звонка, а второе – для понимания, что пользователь во всплывающем окне нажал “Отмена”.

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

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

На схеме видно, что мы разбили процесс начала звонка на два этапа. В начале, по willResignActive мы переводим звонок в CallStarting. А потом, после события didEnterBackground мы считаем, что звонок начался.

После разговора, происходит тоже самое, но в обратном порядке. По событию willEnterForeground отмечаем, что звонок завершается. А после didBecomeActive, когда приложение снова целиком наше, мы считаем, что звонок полностью завершен и система готова к следующему. Мы не знаем как прошел звонок пользователя. Поговорил ли он, был ли абонент занят или просто не снял трубку. Мы отмечаем, только факт попытки. 

Давайте вернемся немного назад, к негативному сценарию. Когда пользователь в алерте нажал “Отмена”. В такой ситуации мы просто вернемся в приложение и больше ничего не произойдет. А наш трекер через секунду перейдет в состояние None.

Стейт-машина в деле: реализация

С проектированием разобрались, переходим к реализации. Я не буду полностью показывать код всего трекера. Остановлюсь только на одном интересном моменте: некоторые события жизненного цикла в нашей схеме приходят дважды. Например, didBecomeActive. Первый раз, после отображения алерта: 

И второй после окончания звонка:

Когда мы находимся в состоянии SystemAlert, по приходу didBecomeActive запускается таймер, и мы переходим в состояние Waiting. А из состоянии CallEnded мы переходим в None и отправляем сигнал, что звонок завершен. Если в любом другом стейте нам приходит состояние didBecomeActive, мы сбрасываем наш трекер в None. Значит, что-то пошло не так. 

Как видите, сам паттерн стейт-машины можно применять без каких-либо дополнительных библиотек. На его основе можно легко собрать enum или структуру UIState для описания компонентов экрана и для их изменения. В CallTracker мы использовали только enum – для описания состояния, и методы – для описания сигналов-переходов. 

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

Где еще пригодится стейт-машина

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

Возьмем что-то совсем из другой области, например, музыкальный плеер. В его стейт войдут: состояние плеера, играет ли сейчас музыка или нет, плейлист, метаданные выбранной композиции и возможно какая-нибудь временная метка. Входящими событиями будут: нажатия на кнопки play, pause, stop, переход к следующему треку и так далее.Мы сможем легко обрабатывать неочевидные действия пользователя. Нажатия несколько раз на stop будут игнорироваться. А нажатия несколько раз на play будут запускать или останавливать музыку, в зависимости от текущего стейта. 

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

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

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

Что в итоге

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

Теги:
Хабы:
Всего голосов 9: ↑8 и ↓1+7
Комментарии5

Публикации

Информация

Сайт
hh.ru
Дата регистрации
Дата основания
Численность
501–1 000 человек
Местоположение
Россия