A máquina de estado raramente é usada pelos desenvolvedores móveis. Embora a maioria conheça os princípios do trabalho e o implemente facilmente de forma independente. No artigo, descobriremos quais tarefas são resolvidas pela máquina de estado usando o exemplo de aplicativos iOS. A história é aplicada na natureza e é dedicada aos aspectos práticos do trabalho.
Abaixo do corte, você encontrará uma transcrição expandida do discurso de Alexander Sychev (
Brain89 ) no
AppsConf , no qual ele compartilhou suas opções para usar a máquina de estado no desenvolvimento de aplicativos que não são de jogos.
Sobre o palestrante: Alexander Sychev atua no desenvolvimento de iOS há oito anos, durante os quais participou da criação de aplicativos simples e clientes complexos para redes sociais e setor financeiro. No momento, é líder técnico do Sberbank.
Eles vêm para a programação de muitas áreas, com diferentes experiências e antecedentes, então primeiro lembramos da teoria básica.
Declaração do problema

Uma máquina de estado é uma abstração matemática que consiste em três elementos principais:
- muitos estados internos
- o conjunto de sinais de entrada que determinam a transição do estado atual para o próximo,
- conjuntos de estados finais, após a transição para a qual o autômato conclui seu trabalho ("admite a palavra de entrada x").
Condição
Por estado, queremos dizer uma variável ou um grupo de variáveis que determinam o comportamento de um objeto. Por exemplo, no aplicativo iOS padrão "Configurações
", há um item "Negrito" ("Básico → Acesso universal"). O valor deste item permite alternar entre duas opções para exibir texto na tela do dispositivo.

Ao enviar o mesmo sinal "Alterar o valor da chave seletora
" , obtemos uma reação diferente do sistema: o estilo de fonte usual ou negrito - tudo é simples. Estando em diferentes estados e recebendo o mesmo sinal, um objeto reage de maneira diferente a uma mudança de estado.
Tarefas tradicionais
Na prática, os programadores geralmente encontram uma máquina de estados finitos.
Aplicações de jogos
Esta é a primeira coisa que vem à mente - como parte da jogabilidade, quase tudo é determinado pelo estado atual do jogo. Portanto, a Apple assume o uso de máquinas de estado principalmente em aplicativos de jogos (discutiremos isso em detalhes posteriormente).
O comportamento do sistema ao processar o mesmo sinal, mas com um estado interno diferente, pode ser ilustrado pelos seguintes exemplos. Por exemplo:
● o personagem do jogo pode ter diferentes forças: uma com armadura mecânica e uma pistola a laser, e a outra com pouca força. Dependendo desse estado, o comportamento dos inimigos é determinado: eles atacam ou fogem.

● o jogo está em pausa - não é necessário desenhar o quadro atual; o jogador no modo de menu ou no processo do jogo - a renderização é completamente diferente.
Análise de Texto
Uma das tarefas mais populares de análise de texto associadas ao uso de uma máquina de estado são os filtros de spam. Que haja um conjunto de palavras de parada e uma sequência de entrada. Você deve filtrar essa sequência ou não exibi-la.

Formalmente, esta é a tarefa de encontrar uma substring em uma string. Para resolvê-lo, é utilizado o algoritmo Knut-Morris-Pratt, cuja implementação de software é uma máquina de estados finitos. O estado é o deslocamento da sequência de entrada e o número de caracteres encontrados na palavra de parada padrão.
Além disso,
ao analisar expressões regulares , frequentemente são usadas máquinas de estado finito.
Processamento de consulta paralela
Uma máquina de estado é uma das opções para implementar o processamento de solicitações e executar um conjunto estrito de instruções.

Por exemplo, no servidor da web nginx, as solicitações de entrada de vários protocolos são processadas usando máquinas de estado. Dependendo do protocolo específico, uma implementação específica da máquina de estados é selecionada e, portanto, um conjunto conhecido de instruções é executado.
Em geral, são obtidas duas classes de problemas:
- gerenciar a lógica de um objeto complexo com um estado interno complexo,
- formação de controle e fluxos de dados (descrição do algoritmo).
Obviamente, essas tarefas comuns são encontradas na prática de qualquer programador. Portanto, é possível o uso de uma máquina de estado, inclusive em aplicativos de conteúdo que não sejam de jogos, envolvidos na maioria dos desenvolvedores de dispositivos móveis.
Em seguida, analisaremos onde e quando a máquina de estado pode ser usada para criar aplicativos iOS típicos.
A maioria dos aplicativos móveis possui uma arquitetura em camadas. Existem três camadas de base.
- Camada de apresentação.
- Camada de lógica de negócios.
- Um conjunto de auxiliares, clientes de rede e assim por diante (camada principal).
Como indicado acima, a máquina de estado controla objetos com comportamento complexo, ou seja, com condição complexa. Esses objetos estão definitivamente na camada de apresentação, porque toma decisões processando entradas ou mensagens do usuário do sistema operacional. Vejamos as diferentes abordagens para sua execução.

Na metáfora arquitetônica clássica do Model-View-Controller, o estado estará no controlador: ele decide o que é exibido no View e como responder aos sinais de entrada: pressionando um botão, alterando o controle deslizante e assim por diante. É lógico que uma das implementações do controlador seja uma máquina de estado.

No VIPER, o estado está no apresentador: é ele quem determina a transição de navegação específica da tela atual e a exibição dos dados na Visualização.

No Model-View-ViewModel, o estado está no ViewModel. Independentemente de termos ou não ligantes reativos, o comportamento do módulo definido na metáfora MVVM será registrado no ViewModel. Obviamente, sua implementação através de uma máquina de estado é uma opção aceitável.

Objetos complexos com um conjunto não trivial de estados também são encontrados na camada de lógica de negócios do aplicativo. Por exemplo, um cliente de rede que, dependendo se uma conexão com o servidor está ou não estabelecida, envia ou bloqueia solicitações. Ou um objeto para trabalhar com um banco de dados que precisa converter funções de linguagem em uma consulta SQL, executá-lo, obter uma resposta, traduzi-lo em objetos etc.

Em tarefas mais específicas, como um módulo de pagamento, no qual um conjunto mais amplo de estados, lógica complexa, o uso de uma máquina de estados também está correto.
Como resultado, descobrimos que em aplicativos móveis existem muitos objetos cujo estado e lógica de comportamento são descritos mais complicados do que com uma frase. Eles devem ser capazes de gerenciar.
Considere um
exemplo real e entenda em que ponto uma máquina de estado finito é realmente necessária e onde sua aplicação não é justificada.

Considere o ViewController do aplicativo Championship iOS, um recurso esportivo popular. Este controlador exibe um conjunto de comentários em forma de tabela. Os usuários inserem a descrição da partida, visualizam fotos, leem as notícias e deixam seus comentários. A tela é bastante simples: a camada subjacente fornece dados, é processada e exibida na tela.

Dados reais ou um erro podem ser transmitidos para o display. Portanto, o primeiro operador condicional aparece, o primeiro ramo, que determina o comportamento adicional do aplicativo.
A próxima pergunta é o que fazer se não houver dados. Esta condição é um erro? Provavelmente não: nem todas as notícias têm comentários de usuários. Por exemplo, o hóquei no Egito é de pouco interesse para qualquer pessoa; nesse artigo, geralmente não há comentários. Esse é o comportamento normal e o estado normal da tela que você precisa para poder exibir. Portanto, o segundo operador condicional aparece.
É lógico supor que também existe um estado inicial no qual o usuário espera dados (por exemplo, quando a tela de comentários aparece apenas na tela). Nesse caso, exiba o indicador de carregamento corretamente. Esta é a terceira declaração condicional.
Portanto, já temos quatro estados em uma tela simples, cuja lógica de exibição é descrita através da construção if-else-if-else.

Mas e se houver mais desses estados? O desenvolvimento iterativo da tela leva a um emaranhado intrincado de construções condicionais, um monte de sinalizadores ou uma caixa de comutação múltipla complicada. Este código é assustador. Imagine que o desenvolvedor que o apoiará sabe onde você mora e ele tem uma serra elétrica que ele sempre carrega com ele. E você realmente quer viver de acordo com sua pensão pequena, mas bem merecida.
Penso que, neste caso, vale a pena considerar se vale a pena deixar essa implementação no aplicativo.
Desvantagens
Vamos entender o que não gostamos neste código.

Primeiro de tudo, é
difícil de ler .
Como o código é mal lido, significa que será difícil para um novo desenvolvedor descobrir o que exatamente é implementado em um determinado local do projeto. Dessa forma, gastará muito tempo analisando a lógica de comportamento do aplicativo - o
custo do suporte e do desenvolvimento aumentará .
Este código não é
flexível . Se você precisar adicionar um novo estado que não segue da escada atual, ele pode não ter êxito! Se você precisar de uma passagem direta - pare abruptamente de passar nas verificações nesta escada - como fazê-lo? Quase nada.
Além disso, com essa abordagem,
não há proteção contra estados fictícios . Quando as transições são descritas através de um caso de opção, provavelmente o comportamento padrão é implementado. Esse estado é lógico em termos de comportamento do programa, mas dificilmente lógico em termos da lógica humana ou comercial do aplicativo.
Qual pode ser a solução para as deficiências indicadas? Obviamente, essa é a construção da lógica de cada módulo / controlador / objeto complexo, não com base na intuição, mas usando uma boa abordagem formalizada. Por exemplo, uma máquina de estados finitos.
Gameplaykit
Como
exemplo, tomamos o que a Apple oferece. Dentro da estrutura GameplayKit, existem duas classes que nos ajudam a trabalhar com a máquina de estado.
O nome da estrutura deixa claro que a Apple queria ser usada em jogos. Mas
em aplicativos que não sejam de jogos, será útil.

A classe
GKState define estado. Para descrevê-lo, você precisa executar etapas simples. Herdamos dessa classe, definimos o nome do estado e definimos três métodos.
- isValidNextState - se o estado atual é válido com base no anterior.
- didEnterFrom - ações na transição para esse estado.
- willExitTo - ações ao sair desse estado.
GKStateMachine é uma classe de máquina de estado. É ainda mais fácil. É o suficiente para executar duas ações.
- Passamos o conjunto de estados de entrada para o array digitado através do inicializador.
- Fazemos transições dependendo dos sinais de entrada usando o método enter. Por meio dele, o estado inicial também é definido.
Pode ser confuso que qualquer classe seja passada como argumento para o método
enter . Mas deve-se notar que um objeto de qualquer classe não pode ser definido em uma matriz de estados
- isso proíbe a digitação estrita. Portanto, se você definir uma classe arbitrária como a próxima classe de estado, nada acontecerá e o método enter retornará false.
Estados e transições entre eles
Tendo se familiarizado com a estrutura da Apple, voltemos ao exemplo. É necessário descrever os estados e transições entre eles. Você precisa fazer isso da maneira mais compreensível. Existem duas opções comuns: uma tabela ou como um gráfico de transição. O gráfico de transição, na minha opinião, é uma opção mais compreensível. Está na UML de maneira padronizada. Portanto, nós escolhemos.
No gráfico de transição, existem estados descritos por nomes e setas que conectam esses estados para descrever transições. No exemplo, existe um estado inicial
- estamos esperando dados
- e há três estados que podem ser alcançados desde o inicial: dados recebidos, nenhum dado e erro.

Na implementação, temos quatro classes pequenas.

Vamos analisar o estado "Dados pendentes". Na entrada, vale a pena exibir o indicador de download. E quando você sair desse estado
, oculte-o. Para fazer isso, você precisa ter um link fraco para o ViewController, que é controlado pela máquina de estado criada.

Parâmetros da máquina
O segundo passo que precisa ser feito
é definir os parâmetros da máquina de estado. Para fazer isso, crie estados e transfira-os para o objeto de autômato.

Também não se esqueça de definir o estado inicial

Em princípio, tudo, a máquina está pronta. Agora é necessário processar reações a eventos externos, alterando o estado do autômato.

Lembre-se da declaração do problema. Temos uma escada do if-else, com base na qual foi decidido que ação deve ser executada. Como controle de um autômato simples, essa opção de implementação pode ser (de fato, uma opção simples
- essa é uma implementação primitiva de uma máquina de estados finitos), mas praticamente não nos livramos das desvantagens mencionadas anteriormente.
Existe outra abordagem que permitirá que você se afaste dessas escadas. É proposto pelos clássicos da programação
- a chamada "quadrilha dos quatro".

Existe um padrão de design especial, chamado "Status".

Esse é um padrão comportamental semelhante a uma estratégia que descreve uma abstração de máquina de estado. Permite que o objeto altere seu comportamento, dependendo do estado. O principal objetivo do aplicativo
é encapsular o comportamento e os dados associados a um estado específico em uma classe separada. Assim, a máquina de estado, que inicialmente tomou a decisão de qual estado causar, agora transmitirá um sinal, o converterá em um estado, e o estado tomará uma decisão. Então, descarregue parcialmente a escada e o código se tornará mais agradável de usar.
A estrutura padrão não sabe como. Ele sugere que a
GKStateMachine tome a decisão. Portanto, expandimos a máquina de estados finitos com um novo método, onde, como configuração, passamos a descrição de todas as variáveis condicionais que determinam exclusivamente o próximo estado. Dentro deste método, você pode delegar a seleção do próximo estado para o atual.

É uma boa prática descrever o estado com um objeto e sempre transmiti-lo, em vez de escrever muitos parâmetros de entrada. Em seguida, delegamos a escolha do próximo estado para o atual. Essa é a atualização completa.
Vantagens do GameplayKit.- Biblioteca padrão. Não há necessidade de baixar nada, use cocoapods ou cartago.
- A biblioteca é bastante fácil de aprender.
- Existem duas implementações ao mesmo tempo: no Objective-C e no Swift.
Desvantagens:- Realizações de estados e transições estão intimamente relacionadas.
O princípio da responsabilidade exclusiva é violado: o Estado sabe para onde vai e como. - Estados duplicados não são controlados de forma alguma.
Uma matriz é passada para a máquina de estados, não muitos estados. Se você transferir vários estados idênticos, o último da lista será usado.
O que mais são as implementações de máquinas de estados finitos? Dê uma olhada no GitHub.
Implementações de Objective-C

TransitionKit
Essa é a biblioteca Objective-C mais popular por um longo tempo, sem deficiências identificadas no GamePlayKit. Ele nos permite implementar uma máquina de estado e todas as ações associadas a ela em blocos.
O estado é separado das transições .
No TransitionKit, existem 2 classes.
- TKState - para definir estados e ações de entrada e saída.
- TKEvent é uma classe para descrever a transição.
O TKEvent liga alguns estados a outros. O evento em si é definido simplesmente por uma string.
Além disso, há benefícios adicionais.
Você pode transferir dados úteis durante a transição . Isso funciona da mesma maneira que ao usar o NSNotificationCenter. Toda carga útil vem na forma de um dicionário userInfo e o usuário analisa as informações.
A transição errônea tem uma descrição . Quando tentamos fazer uma transição inexistente - impossível - obtemos não apenas o valor NO ao retornar do método de transição, mas também uma descrição detalhada do erro, que é útil ao depurar uma máquina de estado.

O TransitionKit é usado no popular harvester da rede RestKit. Este é um bom exemplo de como uma máquina de estado pode ser usada no kernel do aplicativo ao implementar operações de rede.

O RestKit possui uma classe especial - RKOperationStateMachine - para gerenciar operações simultâneas. Na entrada, ele aceita a operação que está sendo processada e a fila para sua execução.

Internamente, a máquina de estados é muito simples: três estados (pronto, executado, concluído) e duas transições: execução inicial e final. Após o início do processamento (e em qualquer transição), a máquina de estado começa a controlar um bloco de código definido pelo usuário na fila especificada ao criar a fila.
Uma operação associada ao seu autômato transfere eventos externos para o autômato e realiza transições entre estados e todas as ações relacionadas. Máquina de estado cuida de
- execução de código assíncrono,
- execução de código atômico durante transições,
- controle de transição
- Cancelamento de operações
- a correção das variáveis de estado da mudança de operação: isReady, isExecuting, isFinished.
Mudança
Além do TransitionKit, vale mencionar separadamente
Shift - uma pequena biblioteca implementada como uma categoria no NSObject. Essa abordagem permite transformar qualquer objeto em uma máquina de estado, descrevendo seu estado na forma de constantes de string e ações em blocos durante transições. Obviamente, este é mais um projeto de treinamento, mas bastante interessante e permite que você experimente o que é uma máquina de estado a um custo mínimo.
Implementações rápidas

Existem muitas implementações de máquinas de estados finitos no Swift. Vou destacar um (
observação : infelizmente, o projeto não está sendo desenvolvido nos últimos dois anos após o relatório, mas vale a pena contar as idéias nele contidas no artigo).
SwiftyStateMachine
No SwiftyStateMachine, a máquina de estado é representada por uma estrutura não estável; por meio dos métodos didSet da propriedade, você pode capturar facilmente as alterações de estado.
Nesta biblioteca, a máquina de estados é definida através da tabela de correspondência de estados e transições entre eles. Este esquema é descrito separadamente do objeto que a máquina controlará. Isso é implementado através de uma caixa de opções aninhada.

, .
- .
, . - .
state machine , state machine. - , , .
- , DOT.
state- — DOT. , , .

Conclusão
.
- .
, . , . , .
- .
( ).
- .
, , . - . , SwiftyStateMachine , , . .
- .
, . , , . .
.

. , . , , switch case: , , — .

. . , . , , , . .

, , . .

—
. : , — .


«-»
.

, . .
app coordinators — , , : , . , .
, app coordinator , state machine. . , app coordinators state machine, , , ,
. , , , . .

, state machine , , .
state machine , if-else. , .
Apps Conf 2018, 8 9 , - .
, YouTube- . , .