Управление состояниями (или конечными автоматами)

В своей первой теме я буду обсуждать конечные автоматы. Это простая концепция, но я вижу, что многие люди не знают или не понимают. Я уже говорил об этом в Cocoa Heads SP (он на португальском языке и ориентирован на iOS). В этой статье я буду придерживаться более широкого подхода. Весь код будет написан на Swift, но его концепции применимы в любом сценарии (действительно, многое можно представить в виде конечного автомата).

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

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

Значит, совсем не страшно? Конечно, это простейший конечный автомат, и если вам больше интересны конечные автоматы в схемах и теории компьютеров, эта статья может помочь вам понять больше.

Но когда мы начнем реальные примеры программирования? Итак, давайте погрузимся в другие примеры ежедневного программирования:

В приведенном выше коде показан простой пример переключателя. Я не думал о более сложном примере, потому что все мы делаем это каждый день в программировании. Обычно это контрольный флаг, который мы используем в ifs вокруг кода, чтобы сделать что-то, если оно истинно, или сделать что-то еще. Конечно, это логическое управление может быть действительно неприятным, и с большим количеством логических значений мы вызываем печально известный pyramid of doom (or Hadouken code), когда мы управляем многими вещами с помощью логических значений в каком-то блоке кода.

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

Я уже говорил об использовании логического флага для управления вашими состояниями, но представьте себе что-то вроде Super Mario Bros. Простая игра с простой механикой: прыгай, беги, а если поймаешь цветок, атакуй. При только наивном подходе у нас есть хотя бы для флагов: isJumping, isRunning, hasCatchFlower, isAttacking. Но у нас также есть, когда он умер, когда он был маленьким, когда ловили звезду ... не так много веселья впереди, верно? (Я не знаю исходный код Super Mario Bros, но, вероятно, они обрабатывали множество флагов, поскольку исходный код находится в Assembly 6502)

Итак, как мы будем справляться со всеми этими состояниями? Следующий шаг - не совсем новое: перечисления. Для тех, кто не знает, enums (сокращение от перечислений) - это функция языков, начиная с C (может быть даже старше, я не знаю, но я думаю, что цитирование языка из 70-х достаточно например). Но тогда, что мы делаем, чтобы использовать enums?

Намного больше кода, чем в предыдущем примере, верно? Если это звучит более сложно, мы смотрим на упрощенную версию игры 1985 года. Представьте себе такие игры, как Megaman X или The Legend Of Zelda: A Link to the Past (заставляют меня дрожать от мыслей, но это тоже хороший вызов 😉). Для тех, кто не знаком с этими играми, просто бегло посмотрите на YouTube, и вы найдете различные игровые видео.

Обратной стороной этого подхода является то, что вам нужно работать с большим количеством switch statements, и это может быть очень повторяющимся и болезненным для обслуживания. Кроме того, вы можете смешивать множество обязанностей в одном месте (я подделал некоторые методы, которые содержали бы реальную логику, но реальный мир не всегда так прекрасен).

Хорошо, некоторые могут подумать: «Я не делаю игр, зачем мне это использовать?». Если вы фронтенд разработчик, возможно, вы сможете использовать их в: отображаемом содержании, загрузке и ошибка. Или кнопка, на которой: нажата, выделена, нормально. Не похоже на ежедневную работу? Если вы серверный разработчик, это может быть статус покупки в электронной коммерции. Или даже среды: разработка, гомолог, производство. И, как я уже сказал, все можно смоделировать как состояния: сообщение получено, сообщение отправлено; бодрствовать, работать, спать; рождение, жизнь, смерть (☠️)… Думаю, не нужно больше примеров, правда?

После написания я заметил, что многие мои примеры основаны на 3 состояниях, но могут быть любым числом состояний N, поскольку N - конечное число. Так что да, у вас может быть миллиард различных состояний, но это не похоже на то, с чем люди могут справиться (🤷).

И что теперь? Это просто так? Каждый ли теперь хозяин государства? Собственно, показать еще есть что: Государственный образец. Однако разве не все выше государственного образца? Или лучше, разные паттерны состояний? Да, но есть еще один, который основан на ООП, но мы воспользуемся другим подходом и сделаем POP (не музыкальный стиль, а протоколно-ориентированное программирование ). POP - это что-то в основе Swift, и для тех, кто не понимает, в основном это что-то вроде интерфейсов Java / Kotlin или абстрактных классов C ++ (на самом деле есть некоторые различия , но это выходит за рамки данной статьи).

Если вы разработчик iOS, возможно, вы слышали о GameplayKit. В этой структуре Apple уже реализован и бесплатный для использования полный шаблон состояния (также это тема моего выступления с начала этого текста) под названием GKStateMachine. Если вам интересно, у меня есть собственная реализация GKStateMachine. Если вы не разработчик iOS, я также рекомендую посмотреть код, он несложный и может быть легко перенесен на другой язык.

Итак, что такое State Pattern, использующий POP? Давайте посмотрим (вот и длинная суть, извините 😅):

Много чего происходит. Кроме того, это можно рассматривать как чрезмерную инженерию или что-то в этом роде. Но помните, это для сложных случаев, когда есть много состояний или также сложное поведение, и вам нужно очень четкое разделение ответственности (кто-нибудь думает о S для SOLID? ). Например, в такой игре, как Super Mario Bros., о которой я упоминал ранее, это действительно помогло бы, поскольку в каждом состоянии вы можете установить анимацию игрока, физику и т. Д.

Для тех, кто не понимает всего, что происходит, в основном конечный автомат - это Тип значения, и чтобы изменить его содержимое каким-либо методом, вам нужно указать, что это mutating. Я использую metatypes, чтобы решить, в какое состояние мне войти. Метатипы - это типы, которые описывают типы языка (сбивает с толку?). Возьмем пример String: мы все знаем, что такое строка, но представьте, что мы хотим ссылаться на тип String, а не на его содержимое, как бы мы это сделали? И тогда введите метатипы. Вы можете ссылаться на сам тип, а не на его содержимое. Это действительно полезно в ситуациях, подобных описанной выше, когда тип уже знает, что будет делать, вам просто нужно получить нужный тип, и он сделает то, что нужно сделать.

Точно так же этот конечный автомат устойчив к ошибкам, подобным в последней строке (попытка войти в состояние, которое не отображается). Но вы можете изменять и обрабатывать каждый случай любым удобным для вас способом (приводить к сбою, отображать ошибку…). И вам действительно не нужен конечный автомат, как видите, он немного продвинутый (обработка метатипов может быть сложной для новичков). Альтернативой может быть сохранение каждого состояния в отдельной переменной или «держателе» (я не знаю, сможет ли кто-нибудь создать тип для чего-то вроде этого 🤔, но вы можете ожидать чего угодно от людей. ).

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

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

Просто подкрепляю то, что я сказал в начале: да, конечные автоматы применяются повсюду. Искусственный интеллект, печатные платы, математические модели, физические модели, компьютерная графика (OpenGL работает как конечный автомат, а другие графические API-интерфейсы также используют концепции состояний) и т.д. более сложные задачи, конечные автоматы могут быть действительно удобными и полезными.

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

Если вам интересно узнать о паттернах состояний в разных парадигмах программирования, здесь - пример на C, а здесь - на языке Haskell. Я считаю важным показать в разных парадигмах, чтобы увидеть, что даже если мой код написан на Swift, он не ограничивается языком или парадигмами, которые реализует Swift.