Máquinas de estado em Python

Fernando Macedo

@fgmacedo | fgmacedo@gmail.com

Agenda

  • Case de máquina de estados real.
  • Definições sobre máquina de estados.
  • Sinais de que você precisa de uma.
  • Exemplos.

Case: Integração com o novo sorter

Cenário:

  • Alta disponibilidade.
  • Baixa latência.

Case: Integração com o novo sorter

arquitetura-wop

Máquinas de estados

Não é sobre: DFA, NFA, GNFA, Moore, Mealy, classificações...

Máquinas de estados

  • Um objeto sempre está em apenas um dos possíveis estados, e existem transições mapeadas entre estes estados.
  • A alteração de estado se dá em resposta a eventos externos.
  • A resposta para um evento geralmente depende do tipo de evento e do estado interno do sistema, e pode implicar numa transição de estado.

Considere um semáforo:

semaphore

  • Cada um dos círculos é um estado;
  • Cada seta é uma transição de estado possível;
  • O nome associado com a seta representa um evento.

Eventos

  • Ações que afetam o sistema, os gatilhos ou triggers.
  • Podem conter parâmetros.

semaphore

Estados

  • Representam um comportamento do sistema;
  • Uma condição de execução, que simplifica a verificação das ações possíveis para uma única variável.

semaphore

Vencendo a barreira

  • Por que são tão pouco utilizadas?
  • Quantas vezes você já implementou uma solução baseada em máquina de estados?

Justificativas

  • Não identificar o padrão.
  • Objetos que evoluem ao longo do tempo.
  • Existe o campo de estado, mas o controle é feito manualmente.
  • A complexidade atual é suficiente para cobrir o esforço de portar?
  • Curva de aprendizado para incorporar uma nova ferramenta.

Aplicações

  • Sempre que existe mudança de comportamento quando um estado interno muda;
  • É especialmente útil em sistemas orientados a eventos;
  • Auxilia a descobrir casos de uso e comportamentos não previstos.

Identificando o padrão

Sintomas de que você pode precisar de uma máquina de estados:

  • Ter um campo state ou status no seu modelo.
  • Campos booleanos, como published, paid, started, finished.
  • Campos de timestamp que permitem nulos, como published_at, paid_at.
  • Quando existem muitas operações com verificações condicionais “protegendo” sua execução.

Implementações

hand made

  • Padrão State (Gang of Four)
  • Dicionário com o mapa das transições.

Implementações

transitions

Implementações

automaton

Implementações

python-statemachine

semaphore

In [1]:
from statemachine import StateMachine, State

class TrafficLightMachine(StateMachine):
    "A traffic light machine"
    green = State('Green', initial=True)
    yellow = State('Yellow')
    red = State('Red')

    slowdown = green.to(yellow)
    stop = yellow.to(red)
    go = red.to(green)

    def on_slowdown(self):
        print('Calma, lá!')

    def on_stop(self):
        print('Parou.')

    def on_go(self):
        print('Valendo!')
In [2]:
stm = TrafficLightMachine()

stm.slowdown()
stm.stop()
stm.go()
Calma, lá!
Parou.
Valendo!
In [3]:
stm.is_green
Out[3]:
True
In [4]:
try:
    stm.stop()
except Exception as e:
    print(e, type(e))
Can't stop when in Green. <class 'statemachine.exceptions.TransitionNotAllowed'>

semaphore cycle

In [5]:
from statemachine import StateMachine, State

class TrafficLightMachine(StateMachine):
    "A traffic light machine"
    green = State('Green', initial=True)
    yellow = State('Yellow')
    red = State('Red')

    cycle = green.to(yellow) | yellow.to(red) | red.to(green)

    def on_enter_green(self):
        print('Valendo!')

    def on_enter_yellow(self):
        print('Calma, lá!')

    def on_enter_red(self):
        print('Parou.')
In [6]:
stm = TrafficLightMachine()

stm.cycle()
stm.cycle()
stm.cycle()
Calma, lá!
Parou.
Valendo!

Solucionando o problema do sorter

class PackageStateMachine(StateMachine):
    # States
    created = State('Criado', initial=True)
    scanned = State('Escaneado')
    measured = State('Medido')
    waiting_routing = State('Aguardando roteirização')
    routed = State('Roteirizado')
    dispatched = State('Expedido')
    rejected = State('Rejeitado')
    unfit = State('Fora do perfil')
    cancelled = State('Cancelado')

Definimos as transições e os eventos:

# transitions
    scan = (
        created.to(scanned, rejected) |
        rejected.to(scanned) |
        unfit.to(scanned, rejected)
    )
    measure = scanned.to(measured, unfit)
    sort = (
        measured.to(waiting_routing, rejected) |
        unfit.to(unfit) |
        rejected.to(unfit)
    )
    route = waiting_routing.to(routed)
    dispatch = routed.to(dispatched)
    status_changed = (
        created.to(created, cancelled) |
        dispatched.to(dispatched)
    )
    update = created.to(created) | rejected.to(rejected)

Obrigado!

Fernando Macedo

@fgmacedo | fgmacedo@gmail.com

Hey, estamos contratando!