Isenção de responsabilidade: Este artigo descreve uma solução não óbvia para um problema não óbvio. Antes de correr ovos colocá-lo em prática, recomendo ler o artigo até o fim e pensar duas vezes.

Olá pessoal! Ao trabalhar com código, geralmente precisamos lidar com o estado . Um desses casos é o ciclo de vida dos objetos. Gerenciar um objeto com vários estados possíveis pode ser uma tarefa não trivial. Adicione a execução assíncrona aqui e a tarefa é complicada por uma ordem de magnitude. Existe uma solução eficaz e natural. Neste artigo, falarei sobre a máquina de eventos e como implementá-la no Go.
Por que gerenciar o estado?
Para começar, vamos definir o próprio conceito. O exemplo mais simples de um estado: arquivos e várias conexões. Você não pode simplesmente pegar e ler um arquivo. Ele deve primeiro ser aberto e, no final de preferência não se esqueça de fechar. Acontece que a ação atual depende do resultado da ação anterior: a leitura depende da abertura. O resultado salvo é o estado.
O principal problema com o estado é a complexidade. Qualquer estado complica automaticamente o código. Você precisa armazenar os resultados das ações na memória e adicionar várias verificações à lógica. É por isso que as arquiteturas sem estado são tão atraentes para os programadores - ninguém quer problemas dificuldades. Se os resultados de suas ações não afetarem a lógica de execução, você não precisará de um estado.
No entanto, há uma propriedade que faz você contar com as dificuldades. Um estado exige que você siga uma ordem específica de ações. Em geral, essas situações devem ser evitadas, mas isso nem sempre é possível. Um exemplo é o ciclo de vida dos objetos do programa. Graças ao bom gerenciamento de estado, é possível obter um comportamento previsível de objetos com um ciclo de vida complexo.
Agora vamos descobrir como fazer isso legal .
Automático como forma de resolver problemas

Quando as pessoas falam sobre estados, as máquinas de estados finitos vêm imediatamente à mente. É lógico, porque um autômato é a maneira mais natural de gerenciar um estado.
Não vou me aprofundar na teoria dos autômatos : há informações mais do que suficientes na Internet.
Se você procurar exemplos de máquinas de estado finito para o Go, certamente encontrará um lexer de Rob Pike . Um ótimo exemplo de autômato no qual os dados processados são o alfabeto de entrada. Isso significa que as transições de estado são causadas pelo texto que o lexer processa. Solução elegante para um problema específico.
O principal a entender é que um autômato é uma solução para um problema estritamente específico. Portanto, antes de considerá-lo um remédio para todos os problemas, você deve entender completamente a tarefa. Especificamente, a entidade que você deseja controlar:
- estados - ciclo de vida;
- eventos - o que exatamente causa a transição para cada estado;
- resultado do trabalho - dados de saída;
- modo de execução (síncrono / assíncrono);
- principais casos de uso.
O lexer é bonito, mas apenas muda de estado devido aos dados que ele próprio processa. Mas e a situação em que o usuário chama transições? É aqui que a máquina do evento pode ajudar.
Exemplo real
Para deixar mais claro, analisarei um exemplo da biblioteca phono
.
Para uma imersão completa no contexto, você pode ler o artigo introdutório . Isso não é necessário para este tópico, mas ajudará a entender melhor o que estamos gerenciando.
E o que estamos gerenciando?
phono
é baseado no pipeline DSP. Consiste em três estágios de processamento. Cada estágio pode incluir de um a vários componentes:

pipe.Pump
(bomba inglesa) é um estágio obrigatório de recebimento de som, sempre apenas um componente.pipe.Processor
(manipulador inglês) - um estágio opcional do processamento de som, de 0 a N componentes.pipe.Sink
(pia inglesa) - um estágio obrigatório de transmissão de som, de 1 a N componentes.
Na verdade, gerenciaremos o ciclo de vida do transportador.
Ciclo de vida
É assim que o diagrama de estado do pipe.Pipe
parece.

Itálico indica transições causadas pela lógica de execução interna. Negrito - transições causadas por eventos. O diagrama mostra que os estados são divididos em 2 tipos:
- estados inativos -
ready
e paused
, você só pode pular deles por evento - estados ativos -
running
e pausing
, transições por evento e devido à lógica de execução
Antes de uma análise detalhada do código, um exemplo claro do uso de todos os estados:
Agora, as primeiras coisas primeiro.
Todo o código fonte está disponível no repositório .
Estados e Eventos
Vamos começar com a coisa mais importante.
Graças a tipos separados, as transições também são declaradas separadamente para cada estado. Isso evita a enorme salsichas funções de transição com switch
aninhadas. Os próprios estados não contêm nenhum dado ou lógica. Para eles, você pode declarar variáveis no nível do pacote para não fazer isso sempre. A interface de state
é necessária para o polimorfismo. Falaremos sobre activeState
e idleState
pouco mais tarde.
A segunda parte mais importante da nossa máquina são os eventos.
Para entender por que o tipo de target
é necessário, considere um exemplo simples. Criamos um novo transportador, está ready
. Agora execute-o com p.Run()
. O evento de run
é enviado para a máquina, o pipeline entra no estado de running
. Como descobrir quando o transportador está terminado? É aqui que o tipo de target
nos ajudará. Indica o estado de descanso esperado após o evento. Em nosso exemplo, após a conclusão do trabalho, o pipeline entrará novamente no estado ready
. A mesma coisa no diagrama:

Agora, mais sobre os tipos de estados. Mais precisamente, sobre as activeState
e activeState
. Vejamos as funções listen(*Pipe, target) (state, target)
para diferentes tipos de estágios:
pipe.Pipe
possui funções diferentes para aguardar uma transição! O que tem ai?
Assim, podemos ouvir diferentes canais em diferentes estados. Por exemplo, isso permite que você não envie mensagens durante uma pausa - simplesmente não ouvimos o canal correspondente.
Construtor e partida da máquina

Além das opções de inicialização e funcionais , há o início de uma goroutine separada com o ciclo principal. Bem, olhe para ele:
O transportador é criado e congelou em antecipação a eventos.
Hora de trabalhar
Ligue para p.Run()
!

running
gera mensagens e é executada até que o pipeline seja concluído.
Pausar
Durante a execução do transportador, podemos fazer uma pausa. Nesse estado, o pipeline não gerará novas mensagens. Para fazer isso, chame o método p.Pause()
.

Assim que todos os destinatários receberem a mensagem, o pipeline paused
estado de paused
. Se a mensagem for a última, ocorrerá a transição para o estado ready
.
De volta ao trabalho!
Para sair do estado de paused
, chame p.Resume()
.

Tudo é trivial aqui, o pipeline novamente entra em estado de running
.
Enrolar
O transportador pode ser parado de qualquer estado. Existe p.Close()
.

Quem precisa disso?
Não é para todos. Para entender exatamente como gerenciar o estado, você precisa entender sua tarefa. Há exatamente duas circunstâncias em que você pode usar uma máquina assíncrona baseada em eventos:
- Ciclo de vida complexo - há três ou mais estados com transições não lineares.
- Execução assíncrona é usada.
Embora a máquina de eventos resolva o problema, é um padrão bastante complicado. Portanto, deve ser usado com muito cuidado e somente após um entendimento completo de todos os prós e contras.
Referências