Controlando transações de status com Finite State Machine

Guilherme Biff Zarelli
luizalabs
Published in
10 min readNov 21, 2020

Objetivo: O objetivo é disseminar o State Machine como um padrão de design realmente útil no controle de transações de status de um sistema.

Inspiração: A inspiração desse artigo é apresentar alternativas de design para diferentes problemas enfrentados diariamente, o State Machine se mostrou um excelente padrão para o controle de transações de status possuindo Frameworks concisos para tal prática.

Uma máquina de estados finitos (FSMs) é um modelo de computação baseado em uma máquina hipotética composta de um ou mais estados. Apenas um único estado desta máquina pode estar ativo ao mesmo tempo. Isso significa que a máquina deve fazer a transição de um estado para outro para realizar diferentes ações.

Sendo considerada um modelo de computação que pode ser usada para simular lógica sequencial, para representar e controlar fluxos de execuções de alterações de estados. As máquinas de estados finitos podem ser usadas para modelar problemas em muitos campos, incluindo matemática, inteligência artificial, jogos ou linguística. As máquinas de estados finitos vêm de um ramo da ciência da computação chamado “teoria dos autômatos”.

Introdução

Uma máquina de estados finitos é qualquer dispositivo que armazena o estado de algo em um determinado momento. O estado mudará com base nas entradas, fornecendo a saída resultante para as mudanças implementadas.

Máquinas de estados finitos são amplamente utilizadas em muitos sistemas para descrever a dinâmica de comportamento de uma entidade.

Essa máquina abstrata que pode estar exatamente em um de um número finito de estados a qualquer momento. A máquina de estado pode mudar de um estado para outro em resposta a algumas entradas externas. A mudança de um estado para outro é chamada de transição.

Normalmente, uma máquina de estado é modelada usando um diagrama de estado. Veja um dos mais comuns diagramas de estado utilizados em diversos exemplos, que descreve uma catraca operada por moedas:

Diagrama de representação de uma FSM

Também podemos utilizar uma tabela para determinar os dados de transações dos estados.

Tabela de representação de uma FSM

Por que e Quando utilizar o State Machine

Quando conseguimos visualizar nosso sistema através de diferentes estados possíveis a máquina de estado nos ajuda a imaginar todo o fluxo de transições de estados de uma forma muito simples, pois com ela somos capazes de quebrar grandes tarefas complexas em unidades menores e independentes. Ela nos ajudará a gerenciar de uma forma mais abstrata todas essas unidades.

Já quando não conseguimos visualizar em nosso sistema difentes estados (pensando que seja possível) podemos acabar acoplando nossos componentes um ao outro, com isso escreveremos muitas condições para aplicar tais transições de estado, tornando, por exemplo, os testes de unidade e integrações cada vez mais complexos a modo de garantir que todas essas condições utilizadas em sua implementação sejam seguras.

Com uma máquina de estado já bem definida o desenvolvedor pode se concentrar apenas em definir suas ações, já que a ação realizada durante cada transição é independente uma da outra, tornando inevitavelmente seu sistema mais estável e menos sujeito a alterações.

Concluimos então que em uma implementação de State Machine a configuração de quando um estado pode passar para outro estado fica muito mais clara e objetiva, permitindo nos concentramos na definição sobre o que acontece quando a transição ocorre. Todo trabalho de como isso é feito ficará em sua implementação ou framework de State Machine, transformando o fluxo de trabalho em algo mais previsível.

Show me the Code!

Para por a mão na massa, primeiro precisaremos de um projeto e um problema, assim mostraremos uma solução!

O projeto que será apresentado se chama “PokeCatcher State Machine” desenvolveremos um software em Python que irá simular a captura de Pokémons de maneira iterativa, apresentando o State Machine como o meio para o controle das transações.

Como dito anteriormente precisamos de um problema. Mostraremos então como o código desse jogo poderia ser escrito, não de um modo errado, mas, imperativo e com diversos pontos a serem repensados e melhorados. Veja:

Ao ler o código, notamos uma quantidade considerável de whiles e uma tendência a processos encadeados. Um código dessa maneira, mostra o quão ele é inflexível, e a cada nó adicionado, sua complexidade subirá exponencialmente a ponto de sua manutenção se tornar inviável.

Nota-se que o código é de certa forma orientado a eventos, e que esses eventos são alinhados com um ‘estado’ atual do personagem, cada while representa um estado do jogo e cada if alinhado a ele demonstra a captura de uma ação e sua execução. Dessa maneira podemos mitigá-lo para uma implementação simples de State Machine.

Para iniciarmos a implementação, criaremos um diagrama, no qual, ficará nítido para nós os estados e os eventos que levam a sua mudança.

Diagrama de representação FSM — PokeCatcher
Tabela de representação FSM — PokeCatcher

O diagrama nos dá uma visibilidade melhor do estado inicial, final e do comportamento esperado para cada ação. Com ele podemos iniciar nossa codificação.

O código a seguir mostra a definição dos enums States e Events, também criaremos um objeto Pokemon que será nosso personagem. Ele transacionará o estado da máquina — iniciando sempre pelo seu estado inicial determinado pelo diagrama.

Mapeado os objetos podemos implementar nossas regras se baseando no fluxo de eventos, com isso teremos uma estrutura para receber nosso personagem e a ação desejada.

Para a máquina executar sua função utilizaremos instruções if/elif (em outras linguaguens poderiamos utilizar o switch) no ‘processador de evento’ para determinar a transação a ser executada dado o evento e o estado atual do personagem. Vejamos:

O fluxo de processamento de eventos está pronto, com transações muito bem definidas, eliminando o crescimento horizontal do código. Dessa maneira já temos uma implementação simples de State Machine.

A iteratividade com o usuário muda um pouco, para essa solução. O loop nos permite lidar com vários personagens — nota-se que agora teremos apenas um while com processos menos encadeados.

Esse exemplo simples de State Machine nos mostrou como podemos trabalhar com um flow de eventos muito bem descrito, sem muitos encadeamentos de chamadas e dependências de laços de iterações complexas.

Porém ainda não satisfeito com os elif’s apresentados pelo nosso ‘process_event’ iremos montar uma biblioteca de FSM e desacoplar ainda mais nosso código, deixando-o mais robusto e conciso.

BÔNUS: Construindo uma biblioteca de State Machine

Como refatorar o código feito, desacoplando o sistema para que tenhamos de fato um ‘objeto de State Machine’ para realizar as transações de forma genêrica o suficiente para conseguirmos extrair isso em uma biblioteca?

Bom esse é o desafio que impus dada a implementação simplista da máquina de estado apresentada.

Observando o comportamento do método de processamento notamos um padrão nas operações encadeadas, sempre verificando o estado atual para posteriormente verificar se o evento desejado está configurado a ela, e se estiver, executa ou não uma determinada ação e altera o estado atual para o próximo — Acabamos de descrever a transação de uma máquina de estado!

Dado esses fatos podemos compor nossa classe StateMachineTransaction com atributos de: state, event, event_target, action_method.

O action_method não é necessáriamente obrigatório, mas, podemos defini-lo para podermos executar algo nas transações de estado.

Agora que temos a definição e a classe de transação, precisamos definir o que é de fato nossa máquina de estado para construí-la, para assim conseguirmos elaborar nossa factory.

Pensando superficialmente sobre ela, teremos uma identificação, seu estado atual e sua principal ação que é o envio do evento — Essa é nossa interface!. Sua implementação deve receber o estado inicial os estados finais e a lista de transações.

Vamos comentar sobre o seu principal método send_event que é o envio de um novo evento para a máquina. O argumento message foi inserido para que possamos deixar mais ‘rico’ a execução de uma action (caso exista). Se não houver uma transaction relacionada ao estado e ação atual, o método send_event retornará False , caso contrário retornará True , indicando que existe transaction para tal ação.

O send_event de fato é bem simples, primeiramente podemos verificar o estado atual da máquina e caso esteja finalizada, não há a necessidade de realizar quaisquer processamento, por isso foi criado uma váriavel para manter e controlar o processo de finalização ( __ended). O processo se inicia recuperando a transactionreferente a ação recebida (levando em consideração o estado atual que se mantém como váriavel de classe), verificamos se existe uma action relacionada a ela e a executamos!, dada uma execução com sucesso, a finalização ocorre com a atribuição do target da transaction no estado atual da máquina, e caso, a execução da action falhar (retornar False) o targetnão será reatribuido, permanecendo a máquina com o estado atual.

Estando pronto nosso modelo, precisamos agora criar o Factory. Primeiramente, a implementação da máquina foi deixada como protected justamente para permitir que apenas a Factory a crie — Garantindo uma criação coerente e simples da máquina.

O conceito do Factory para essa lib é permitir com que a criação da máquina fique de fato bem simples e centralizada, deixando a lógica que envolve a construção do dicionário de transactions muito bem abstraída, assim, o usuário, simplismente adiciona novas transações e não tem que pensar, em como elas se relacionam em um nível mais baixo.

As transações são adicionadas em um dicionário tendo como chave o estado e com o valor um outro dicionário contendo eventos associados a transações, dessa forma temos sempre uma lista de eventos associadas a uma transações dentro de um determinado estado.

Quando finalmente utilizarmos o build_machine recuperaremos uma interface da máquina de estados imutável.

Refatorando… Utilizando a biblioteca em nosso código

Finalizaremos essa sessão de refatoração com a implementação de nossa biblioteca de State Machine no código do “PokeCatcher State Machine”.

Um dos pontos que será totalmente reutilizado de nossa ultima implementação são os enums de States e Events , a classe do nosso personagem sofrerá pequenos ajustes, no caso, vamos tirar o controle de estado dela, para que nossa nova máquina de estados fique responsável pelo controle. Deixaremos o nosso domain da seguinte maneira:

Após a definição dos modelos, vamos configurar a StateMachineFactory, faremos isso na main do projeto, para passarmos a Factory injetada na classe principal de nosso jogo.

A configuração da StateMachineFactory ficou bem simples, iniciamos ela dizendo quais estados aceitados ( no caso passamos o enum completo ) e quais estados serão considerados como estados finais da máquina. Após a construção do objeto, passaremos todas as nossas transações, que dizem claramente, de qual e para qual estado ela será executada dada determinado evento.

No exemplo abaixo inseri uma action que será executada na transação do estado HIDDEN para VISIBLE quando o evento FIND ocorrer e na transação do estado de INJURED para DEAD ao receber um ATTACK .

As actions configuradas nessas transações foram inseridas em um arquivo separado no qual iremos deixar uma regra ‘randômica’ na action hidden_to_visible_action baseada no lével do Pokemon, assim, quando o usuário executar a ação de FIND no momento em que o Pokemon estiver no modo HIDDEN, randômicamente a ação poderá ser negada! — Para não estender muito deixaremos apenas um TODO na outra action.

Com a Factory devidamente configurada, vamos refatorar o Main Loop. No arquivo mainpassamos a trabalhar com um objeto PokeCatcherFsm , trabalharemos no método start_main_loop com o mesmo conceito feito anteriormente, o que muda é que não usaremos mais o próprio Pokemon para manter o seu estado atual, agora iremos trabalhar com uma máquina de estado para cada um deles.

Como temos nossa Factory na ponta dos dedos, criaremos uma instância de máquina assim que alguém escolher o Pokemon, e caso, a máquina já esteja criada para ele, basta recuperá-la.

A execução do loop se baseia em:

  • Input de dados: pokemon id+ evento.
  • Recuperar Pokemon pelo id.
  • Recuperar / criar a máquina pelo id do Pokemon.
  • Enviar o evento para máquina.

Conclusão

Com essa biblioteca implementada na mão, ganhamos maturidade para falarmos posteriormente de outras implementações de State Machine e sobre como alguns frameworks trabalham com ele, atualmente o Spring StateMachine em Java possui uma maturidade muito boa e com excelentes recursos, além de ter sido minha inspiração para realizar essa implementação — vale a pena estudá-lo — Também temos otímos recursos na nuvem como as Step Funcions da AWS permitindo implementar regras de State Machine com muita facilidade e maestria.

Concluimos que é muito viável uma implementação de máquina de estados, como visto, conseguimos desacoplar as regras de negócio de um software em uma visão muito clara e bem documentada, acredito ser muito vantajoso essa aplicabilidade em sistemas que trabalham com esse tipo de transições.

Sua aplicabilidade é muito ampla, pensando em baixo nível o mesmo é aplicado para máquinas de regex, verificações de syntax, jogos entre outros, porém, muito pouco difundida no escopo de ‘aplicações enterprise’. Pense por exemplo em um sistema de autorização de pagamentos ou entregas, os estados e ações são muito claros e bem definidos, podemos utilizá-lo para realizar as garantia dessa troca como também enviar notificações aos usuários, permitindo a criação de serviços específicos com um escopo pequeno e muito bem descrito pelas transações que ela proporciona, dessa forma, saimos de gigantes regras de negócio de grandes ou médias API’s para uma enxuta FSM.

--

--