'Estado' é um termo de programação comum que é experimentado por todos os desenvolvedores à medida que avançam do início para a programação de nível intermediário. Então, o que exatamente significa o termo "Estado"?
Em geral, o estado de um objeto é apenas o instantâneo atual do objeto ou uma parte dele. Enquanto isso, na ciência da computação, o estado de um programa é definido como sua posição em relação a entradas previamente armazenadas. Nesse contexto, o termo "estado" é usado da mesma maneira que na ciência: o estado de um objeto, como gás, líquido ou sólido, representa sua natureza física atual e o estado de um programa de computador reflete seus valores ou conteúdos atuais.
As entradas armazenadas são preservadas como variáveis ou constantes em um programa de computador. Ao avaliar o estado de um programa, os desenvolvedores podem examinar os valores contidos nessas entradas. O estado do programa pode mudar durante sua execução - as variáveis podem mudar e os valores da memória podem mudar. Uma variável de controle, como uma usada em um loop, por exemplo, altera o estado do programa a cada iteração. Examinar o estado atual de um programa pode ser usado para testar ou analisar a base de código.
Em sistemas mais simples, o gerenciamento de estado é frequentemente tratado com if-else, if-then-else, instruções try-catch ou sinalizadores booleanos; no entanto, isso é inútil quando há muitos estados imagináveis em um programa. Eles podem levar a um código desajeitado e complicado, difícil de entender, manter e depurar.
Uma desvantagem das cláusulas if-else ou booleanas é que elas podem se tornar bastante extensas, e adicionar outro estado é difícil porque requer reescrever o código de muitas classes diferentes. Suponha que você queira criar um jogo que tenha o menu principal, um loop de jogo e uma tela de conclusão.
Vamos construir um player de vídeo, por exemplo:
class Video: def __init__(self, source): self.source = source self.is_playing = False self.is_paused = False self.is_stopped = True # A video can only be played when paused or stopped def play(self): if not self.is_playing or self.is_paused: # Make the call to play the video self.is_playing = True self.is_paused = False else: raise Exception( 'Cannot play a video that is already playing.' ) # A video can only be paused when it is playing def pause(self): if self.is_playing: # Make the call to pause the video self.is_playing = False self.is_paused = True else: raise Exception( 'Cannot pause a video that is not playing' ) # A video can only be stopped when it is playing or paused def stop(self): if self.is_playing or self.is_paused: # Make the call to stop the video self.is_playing = False self.is_paused = False else: raise Exception( 'Cannot stop a video that is not playing or paused' )
O snippet de código acima é uma implementação if-else de um aplicativo de player de vídeo simples, onde os três estados básicos são - reproduzindo, pausado e parado. No entanto, se tentarmos adicionar mais estados, o código rapidamente se tornará complexo, inchado, repetitivo e difícil de entender e testar. Vamos ver como fica o código ao adicionar outro estado 'rebobinar':
class Video: def __init__(self, source): self.source = source self.is_playing = False self.is_paused = False self.is_rewinding = False self.is_stopped = True # A video can only be played when it is paused or stopped or rewinding def play(self): if self.is_paused or self.is_stopped or self.is_rewinding: # Make the call to play the video self.is_playing = True self.is_paused = False self.is_stopped = False self.is_rewinding = False else: raise Exception( 'Cannot play a video that is already playing.' ) # A video can only be paused when it is playing or rewinding def pause(self): if self.is_playing or self.is_rewinding: # Make the call to pause the video self.is_playing = False self.is_paused = True self.is_rewinding = False self.is_stopped = False else: raise Exception( 'Cannot pause a video that is not playing or rewinding' ) # A video can only be stopped when it is playing or paused or rewinding def stop(self): if self.is_playing or self.is_paused or self.is_rewinding: # Make the call to stop the video self.is_playing = False self.is_paused = False self.is_stopped = True self.is_rewinding = False else: raise Exception( 'Cannot stop a video that is not playing or paused or rewinding' ) # 4. A video can only be rewinded when it is playing or paused. def rewind(self): if self.is_playing or self.is_paused: # Make the call to rewind the video self.is_playing = False self.is_paused = False self.is_stopped = False self.is_rewinding = True else: raise Exception( 'Cannot rewind a video that is not playing or paused' )
Sem o padrão State, você teria que examinar o estado atual do programa em todo o código, incluindo os métodos update e draw. Se você quiser adicionar um quarto estado, como uma tela de configurações, terá que atualizar o código de muitas classes distintas, o que é inconveniente. É aqui que a ideia de máquinas de estado é útil.
As máquinas de estado não são um conceito novo na ciência da computação; eles são um dos padrões de projeto básicos utilizados no negócio de software. É mais orientado ao sistema do que à codificação e é usado para modelar casos de uso.
Vejamos um exemplo simples da vida real de contratar um táxi pelo Uber:
A Tela 1 é a primeira tela que todos os usuários neste caso de uso veem e é independente. A tela 2 depende da tela 1 e você não poderá ir para a tela 2 até fornecer dados precisos na tela 1. Da mesma forma, a tela 3 depende da tela 2, enquanto a tela 4 depende da tela 3. Se nenhum dos dois nem seu motorista cancelar sua viagem, você será movido para a tela 4, onde não poderá planejar outra viagem até que a atual termine.
Digamos que esteja chovendo muito e nenhum motorista aceite sua viagem ou não encontre nenhum motorista disponível em sua região para finalizar sua viagem; uma notificação de erro avisando sobre a indisponibilidade do driver é exibida e você permanece na tela 3. Você ainda pode retornar à tela 2, à tela 1 e até à primeira tela.
Você está em uma etapa diferente do processo de reserva de táxi e só poderá passar para o próximo nível se uma ação especificada no estágio atual for bem-sucedida. Por exemplo, se você inserir o local errado na tela 1, não poderá prosseguir para a tela 2 e não poderá prosseguir para a tela 3, a menos que escolha uma opção de viagem na tela 2, mas poderá volte sempre ao estágio anterior, a menos que sua viagem já esteja reservada.
No exemplo acima, dividimos o processo de reserva de táxi em várias atividades, cada uma das quais pode ou não ser autorizada a chamar outra atividade com base no status da reserva. Uma máquina de estado é usada para modelar isso. Em princípio, cada um desses estágios/estados deve ser autônomo, com um convocando o próximo somente após o término do atual, com sucesso ou não.
Em palavras mais técnicas, a máquina de estado nos permite dividir uma grande ação complicada em uma sucessão de atividades menores separadas, como a atividade de reserva de táxi no exemplo anterior.
Os eventos conectam tarefas menores e a mudança de um estado para outro é chamada de transição. Normalmente, realizamos algumas ações após a mudança de um estado para outro, como criar uma reserva no back-end, emitir uma fatura, salvar dados analíticos do usuário, capturar dados de reservas em um banco de dados, acionar o pagamento após o término da viagem e assim por diante .
Portanto, a fórmula geral para uma máquina de estado pode ser dada como:
Estado Atual + Alguma Ação/Evento= Outro Estado
Vejamos como seria uma máquina de estado projetada para um aplicativo de player de vídeo simples:
E podemos implementá-lo no código usando transições da seguinte forma:
from transitions import Machine class Video: # Define the states PLAYING = 'playing' PAUSED = 'paused' STOPPED = 'stopped' def __init__(self, source): self.source = source # Define the transitions transitions = [ # 1. A video can only be played when it is paused or stopped. {'trigger': 'play', 'source': self.PAUSED, 'dest': self.PLAYING}, {'trigger': 'play', 'source': self.STOPPED, 'dest': self.PLAYING}, # 2. A video can only be paused when it is playing. {'trigger': 'pause', 'source': self.PLAYING, 'dest': self.PAUSED}, # 3. A video can only be stopped when it is playing or paused. {'trigger': 'stop', 'source': self.PLAYING, 'dest': self.STOPPED}, {'trigger': 'stop', 'source': self.PAUSED, 'dest': self.STOPPED}, ] # Create the state machine self.machine = Machine{ model = self, transitions = transitions, initial = self.STOPPED } def play(self): pass def pause(self): pass def stop(self): pass
Agora, caso desejemos adicionar outro estado, digamos rebobinar, podemos fazer isso facilmente da seguinte maneira:
from transitions import Machine class Video: # Define the states PLAYING = 'playing' PAUSED = 'paused' STOPPED = 'stopped' REWINDING = 'rewinding' # new def __init__(self, source): self.source = source # Define the transitions transitions = [ # 1. A video can only be played when it is paused or stopped. {'trigger': 'play', 'source': self.PAUSED, 'dest': self.PLAYING}, {'trigger': 'play', 'source': self.STOPPED, 'dest': self.PLAYING}, {'trigger': 'play', 'source': self.REWINDING, 'dest': self.PLAYING}, # new # 2. A video can only be paused when it is playing. {'trigger': 'pause', 'source': self.PLAYING, 'dest': self.PAUSED}, {'trigger': 'pause', 'source': self.REWINDING, 'dest': self.PAUSED}, # new # 3. A video can only be stopped when it is playing or paused. {'trigger': 'stop', 'source': self.PLAYING, 'dest': self.STOPPED}, {'trigger': 'stop', 'source': self.PAUSED, 'dest': self.STOPPED}, {'trigger': 'stop', 'source': self.REWINDING, 'dest': self.STOPPED}, # new # 4. A video can only be rewinded when it is playing or paused. {'trigger': 'rewind', 'source': self.PLAYING, 'dest': self.REWINDING}, #new {'trigger': 'rewind', 'source': self.PAUSED, 'dest': self.REWINDING}, # new ] # Create the state machine self.machine = Machine{ model = self, transitions = transitions, initial = self.STOPPED } def play(self): pass def pause(self): pass def stop(self): pass def rewind(self): pass
Assim, podemos ver como as máquinas de estado podem simplificar uma implementação complexa e evitar que escrevamos códigos incorretos. Tendo aprendido os recursos das máquinas de estado, agora é importante entender por que e quando usar máquinas de estado.
As State Machines podem ser utilizadas em aplicações que possuem estados distintos. Cada estágio pode levar a um ou mais estados subsequentes, bem como encerrar o fluxo do processo. Uma máquina de estado emprega entrada do usuário ou cálculos no estado para escolher qual estado entrar em seguida.
Muitos aplicativos precisam de um estágio de "inicialização", seguido de um estado padrão que permite uma ampla variedade de ações. As entradas anteriores e atuais, bem como os estados, podem ter um impacto nas ações que são executadas. As medidas de limpeza podem então ser executadas quando o sistema é "desligado".
Uma máquina de estado pode nos ajudar a conceituar e gerenciar essas unidades de forma mais abstrata se pudermos dividir um trabalho extremamente complexo em unidades menores e independentes, onde simplesmente precisamos descrever quando um estado pode fazer a transição para outro estado e o que acontece quando a transição ocorre. Não precisamos nos preocupar em como a transição ocorre após a configuração. Depois disso, só precisamos pensar em quando e o quê, não como.
Além disso, as máquinas de estado nos permitem ver todo o processo de estado de uma forma bastante previsível; uma vez definidas as transições, não precisamos nos preocupar com má administração ou transições de estado errôneas; a transição imprópria pode ocorrer apenas se a máquina de estado estiver configurada corretamente. Temos uma visão abrangente de todos os estados e transições em uma máquina de estado.
Se não usarmos uma máquina de estado, somos incapazes de visualizar nossos sistemas em vários estados possíveis, ou estamos conscientemente ou inconscientemente acoplando nossos componentes firmemente, ou estamos escrevendo muitas condições if-else para simular transições de estado, que complica o teste de unidade e integração porque devemos garantir que todos os casos de teste sejam escritos para validar a possibilidade de todas as condições e ramificações usadas.
As máquinas de estado, além de sua capacidade de desenvolver algoritmos de tomada de decisão, são formas funcionais de planejamento de aplicativos. À medida que os aplicativos ficam mais complexos, cresce a necessidade de um design eficaz.
Diagramas de estado e fluxogramas são úteis e ocasionalmente essenciais ao longo do processo de design. As State Machines são importantes não apenas para o planejamento de aplicativos, mas também são simples de criar.
A seguir estão algumas das principais vantagens das máquinas de estado na computação moderna:
Nem tudo nas máquinas de estado é bom, às vezes elas também podem levar a desvantagens e desafios. Aqui estão alguns dos problemas comuns com máquinas de estado:
Ao usar uma máquina de estado, seu sistema deve idealmente ter dois componentes lógicos:
A máquina de estado pode ser considerada a infraestrutura que conduz as transições de estado; verifica transições de estado e executa ações configuradas antes, durante e depois de uma transição; no entanto, ele não deve saber qual lógica de negócios é executada nessas ações.
Portanto, em geral, é uma boa ideia isolar a máquina de estado da lógica de negócios principal usando abstrações corretas; caso contrário, gerenciar o código seria um pesadelo.
Aqui estão alguns outros cenários da vida real em que precisamos empregar a lógica da máquina de estado com cautela:
A seguir estão algumas das aplicações práticas que se beneficiam do conceito de máquinas de estado em nossas vidas diárias:
Quando você compra algo em um site de comércio eletrônico online, por exemplo, ele passa por várias fases, como Pedido, Embalado, Enviado, Cancelado, Entregue, Pago, Reembolsado e assim por diante. A transição ocorre automaticamente à medida que as coisas passam por um depósito ou centro de logística e são digitalizadas em vários estágios, como quando um usuário cancela ou deseja um reembolso.
A noção de máquinas de estado é extremamente útil na programação. Ele não apenas agiliza o processo de desenvolvimento de aplicativos de caso de uso mais complicados, mas também reduz o trabalho de desenvolvimento necessário. Ele fornece uma compreensão mais simples e elegante dos eventos modernos e, quando aplicado corretamente, pode fazer milagres.