Diretriz: Diagrama de Estados
Diagramas de estado são uma anotação gráfica formal para a especificação das máquinas de estado que serão utilizadas para modelar o comportamento dinâmico dos elementos do modelo. Essa diretriz apresenta essa anotação e demonstra como utilizá-la efetivamente.
Relacionamentos
Descrição Principal

Explicação

As máquinas de estado são utilizadas para modelar o comportamento dinâmico de um elemento de modelo e, mais especificamente, os aspectos direcionados a eventos do comportamento do sistema (consulte Conceito: Eventos e Sinais). As máquinas de estado são utilizadas especificamente para definir o comportamento dependente do estado ou o comportamento que varia de acordo com o estado em que está o elemento de modelo. Os elementos de modelo cujos comportamentos não variam com o seu estado do elemento não precisam de máquinas de estado para descrever seus comportamentos (geralmente, esses elementos são classes passivas cuja responsabilidade principal é gerenciar dados). Especificamente, as máquinas de estado devem ser usadas para modelar o comportamento de classes ativas que usam eventos de chamada e de sinal para implementar suas operações (como as transições na máquina de estado da classe).

Uma máquina de estado consiste em estados, vinculados por transições. Um estado é uma condição de um objeto em que ele realiza alguma tarefa ou espera um evento. Uma transição é um relacionamento entre dois estados que é disparado por algum evento, que executa determinadas ações ou avaliações e que resulta em um estado final específico. Os elementos de uma máquina de estado estão representados na Figura 1.

Diagrama mostrando a anotação da máquina de estado.

Figura 1. Representação da máquina de estado.

Um editor simples pode ser visualizado como uma máquina de estados finitos com os estados Vazio, Aguardando um comando e Aguardando texto. O eventos Carregar Arquivo, Inserir Texto, Inserir Caractere e Salvar e Sair causam transições na máquina de estado. A máquina de estado do editor está representada na Figura 2 abaixo.

Diagrama descrito na legenda.

Figura 2. A máquina de estado de um editor simples.

Estados

Um estado é uma condição de um objeto em que ele realiza alguma tarefa ou espera um evento. Um objeto pode permanecer em um estado durante um tempo limitado. Um estado tem várias propriedades:

Nome Uma seqüência de caracteres textuais que distingue o estado de outros estados; um estado também pode ser anônimo, ou seja, não ter nenhum nome.
Ações de entrada/saída As ações executadas ao entrar no estado ou sair dele.
Transições internas As transições que são manipuladas sem causar mudança de estado.
Subestados A estrutura aninhada de um estado, envolvendo subestados separados (ativos seqüencialmente) ou simultâneos (ativos concomitantemente).
Eventos adiados Uma lista de eventos que não são manipulados no estado, mas são adiados e enfileirados para serem manipulados pelo objeto em outro estado.

Como mostra a Figura 1, existem dois estados especiais que podem ser definidos para a máquina de estado de um objeto. O estado inicial indica o local inicial padrão para a máquina de estado ou o subestado. Um estado inicial é representado como um círculo de preenchimento preto. O estado final indica o término da execução da máquina de estado ou que o estado confinado foi concluído. Um estado final é representado como um círculo de preenchimento preto dentro de um círculo sem preenchimento. Os estados final e inicial são, na verdade, pseudo-estados. Nenhum dos dois pode ter as partes usuais de um estado normal, exceto o nome. Uma transição de um estado inicial para um estado final pode ter o complemento completo de características, incluindo uma condição de guarda e uma ação, mas não pode ter um evento trigger.

Transições

Uma transição é um relacionamento entre dois estados indicando que um objeto no primeiro estado executará certas ações, e entrará em um segundo estado quando ocorrer um evento especificado e determinadas condições forem satisfeitas. Nessa mudança de estado, diz-se que a transição foi 'acionada'. Até que a transição seja acionada, diz-se que o objeto está no estado 'de origem'; após o seu acionamento, diz-se que o objeto está no estado 'de destino'. Uma transição tem várias propriedades:

Estado de origem O estado afetado pela transição. Se um objeto estiver no estado de origem, uma transição de saída poderá ser acionada quando o objeto receber um evento trigger da transição e quando a condição de guarda, se houver, for satisfeita.
Acionador de Eventos O evento que torna a transição passível de ser acionada (contanto que sua condição de guarda seja cumprida) quando recebido pelo objeto no estado de origem.
Condição de Guarda Uma expressão booleana que é avaliada quando a transição é disparada pela recepção do disparador de evento. Se o valor da expressão for Verdadeiro, a transição poderá ser acionada; se a expressão for Falso, a transição não será acionada. Se não houver outra transição que possa ser disparada pelo mesmo evento, o evento será perdido.
Ação Um cálculo indivisível executável que pode agir diretamente sobre o objeto que possui a máquina de estado e indiretamente em outros objetos visíveis ao objeto.
Estado de destino O estado que é ativado após a conclusão da transição.

Uma transição pode ter várias origens (nesse caso, ela representa uma junção de vários estados simultâneos) e vários alvos (nesse caso, ela representa uma forquilha para vários estados simultâneos).

Acionadores de Evento

No contexto da máquina de estado, um evento é uma ocorrência de um estímulo que pode disparar uma transição de estado. Os eventos podem incluir eventos de sinais, eventos de chamada, a passagem do tempo ou uma mudança de estado. Um sinal ou uma chamada pode ter parâmetros cujos valores estão disponíveis para a transição, incluindo expressões para as condições de guarda e a ação. Também é possível haver uma transição sem disparador, representada por uma transição sem trigger de evento algum. Essas transições, também denominadas transições de conclusão, são disparadas implicitamente quando o seu estado de origem concluiu sua tarefa.

Condições de Guarda

Uma condição de guarda é avaliada após o evento do acionador acionar a transição. É possível que haja várias transições do mesmo estado de origem e com o mesmo acionador de evento, contanto que as condições de guarda não se sobreponham. Uma condição de guarda é avaliada apenas uma vez para a transição no momento em que o evento ocorre. A expressão booleana pode fazer referência ao estado do objeto.

Ações

Uma ação é um cálculo indivisível executável, ou seja, ela não pode ser interrompida por um evento e, portanto, é executada até a sua conclusão. Isso é um contraste com uma tarefa, que pode ser interrompida por outros eventos. As ações podem incluir chamadas de operação (para o proprietário da máquina de estado, bem como para outros objetos visíveis), a criação ou a destruição de outro objeto, ou o envio de um sinal para outro objeto. No caso de envio de um sinal, o nome do sinal recebe como prefixo a palavra-chave 'send'.

Ações de Entrada e Saída

As ações de entrada e de saída permitem que uma mesma ação seja enviada sempre que um estado entra ou sai, respectivamente. As ações de entrada e de saída permitem que isso seja feito adequadamente, sem precisar colocar ações explícitas em todas as transições de entrada e de saída. As ações de entrada e de saída não devem ter argumentos ou condições de guarda. As ações de entrada no nível mais alto da máquina de estado para um elemento de modelo podem ter parâmetros que representam os argumentos que a máquina receberá quando o elemento for criado.

Transições Internas

As transições internas permitem que eventos sejam manipulados dentro do estado, evitando, com isso, disparar ações de entrada ou de saída. As transições internas podem ter eventos com parâmetros e condições de guarda e representam, essencialmente, manipuladores de interrupção.

Eventos Adiados

Os eventos adiados são aqueles cuja manipulação é adiada até que um estado em que o evento não seja adiado se torne ativo. Quando esse estado se torna ativo, a ocorrência do evento é disparada e pode causar transições como se ele tivesse acabado de ocorrer. A implementação de eventos adiados requer a presença de uma fila interna de eventos. Quando um evento ocorre mas está listado como adiado, ele é enfileirado. Os eventos sairão da fila tão logo o objeto entre em um estado que não adie esses eventos.

Subestados

Um estado simples é aquele que não possui subestrutura. Um estado que possui subestados (estados aninhados) é denominado estado composto. Os subestados podem ser aninhados em qualquer nível. Uma máquina de estados aninhados deve ter no máximo um estado inicial e um estado final. Os subestados são usados para simplificar máquinas complexas de estados simples mostrando que alguns estados são possíveis apenas dentro de um determinado contexto (o estado confinado).

Diagrama mostrando subestados.

Figura 3. Subestados.

A partir de uma origem externa ao estado composto confinado, uma transição pode ter como objetivo um estado composto ou um subestado. Se o seu alvo for o estado composto, a máquina de estados aninhados deverá conter um estado inicial, para o qual o controle passará após entrar no estado composto e após enviar sua ação de entrada (se houver alguma). Se o seu alvo for o estado aninhado, o controle passará para o estado aninhado após enviar a ação de entrada do estado composto (se houver alguma) e, em seguida, a ação de entrada do estado aninhado (se houver alguma).

Uma transição orientada para fora de um estado composto pode ter como sua origem o estado composto ou um subestado. Nos dois casos, o controle sai primeiro do estado aninhado (e sua ação de saída, se houver, é enviada) e, em seguida, sai do estado composto (e sua ação de saída, se houver, é enviada). Uma transição, cuja origem é o estado composto essencialmente, interrompe a tarefa da máquina de estados aninhados.

Estados do Histórico

A menos que seja especificado de outra forma, quando uma transição entra em um estado composto, a ação da máquina de estado aninhado reinicia novamente o estado inicial (a não ser que o alvo da transição seja um subestado direto). Os estados de histórico permitem que a máquina de estado entre novamente no último subestado que estava ativo antes de sair do estado composto. Um exemplo de uso de estado de histórico é apresentado na Figura 4.

Diagrama mostrando os estados do histórico.

Figura 4. Estado de Histórico.

Técnicas Comuns de Modelagem

Geralmente, as máquinas de estado são usadas para modelar o comportamento de um objeto ao longo de sua vida útil. Elas são especialmente necessárias quando os objetos têm comportamento dependente do estado. Os objetos que podem ter máquinas de estado são classes, subsistemas, casos de uso e interfaces (para declarar estados que devem ser satisfeitos por um objeto que realize a interface). No caso de sistemas em tempo real, as máquinas de estado também são utilizadas para cápsulas e protocolos (para declarar estados que devem ser satisfeitos por um objeto que perceba o protocolo).

Nem todos os objetos requerem máquinas de estado. Se o comportamento de um objeto for simples, de forma que ele apenas armazene ou recupere dados, o comportamento do objeto não varia de estado e sua máquina de estado é de pouco interesse.

A modelagem da vida útil de um objeto envolve três itens: a especificação dos eventos aos quais o objeto pode responder, a resposta a esses eventos e o impacto do comportamento passado no atual. A modelagem da vida útil de um objeto também envolve definição da ordem em que o objeto pode responder coerentemente a eventos, iniciando no momento da criação do objeto e continuando até sua destruição.

Para modelar a vida útil de um objeto:

  • Defina o contexto para a máquina de estado, seja ele uma classe, um caso de uso ou o sistema como um todo.
    • Se o contexto for uma classe ou um caso de uso, colete as classes vizinhas, incluindo as classes pai ou as classes alcançadas por associações ou dependências. Esses vizinhos são possíveis alvos para ações e para inclusão em condições de guarda.
    • Se o contexto for o sistema como um todo, limite o foco a um comportamento do sistema e considere a vida útil dos objetos envolvidos naquele aspecto. A vida útil do sistema inteiro é muito grande para ser um foco significativo.
  • Estabeleça os estados inicial e final para o objeto. Se houver precondições ou pós-condições dos estados inicial e final, defina-as também.
  • Determine os eventos aos quais o objeto responde. Eles podem ser encontrados nas interfaces do objeto. No caso de sistemas em tempo real, eles também podem ser encontrados nos protocolos do objeto.
  • Partindo do estado inicial para o estado final, crie o layout dos estados de nível superior em que os objetos podem estar. Conecte esses estados com as transições acionadas pelos eventos adequados. Continue a acrescentar as transições.
  • Identifique todas as ações de entrada ou de saída.
  • Expanda ou simplifique a máquina de estado usando subestados.
  • Verifique se todos os eventos disparadores de transições na máquina de estado correspondem aos eventos esperados pelas interfaces realizadas pelo objeto. Da mesma forma, verifique se todos os eventos esperados pelas interfaces do objeto são manipulados pela máquina de estado. No caso de sistemas em tempo real, faça verificações equivalentes para os protocolos de uma cápsula. Finalmente, procure os locais em que deseja explicitamente ignorar eventos (por exemplo, eventos adiados).
  • Verifique se todas as ações na máquina de estado são suportadas por relacionamentos, métodos e operações do objeto confinado.
  • Investigue a máquina de estado, comparando-a com as seqüências esperadas de eventos e suas respostas. Pesquise os estados inatingíveis e os estados em que a máquina trava.
  • Caso você reorganize ou reestruture a máquina de estado, verifique se a semântica não foi mudada.

Dicas e Sugestões

  • Quando puder optar, use a semântica visual da máquina de estado em vez de escrever o código de transição detalhado. Por exemplo, não dispare uma transição em vários sinais e depois use um código detalhado para gerenciar o fluxo de controle de modo diferente de acordo com o sinal. Use transições separadas, disparadas por sinais separados. Evite a lógica condicional no código de transição que oculta comportamento adicional.
  • Nomeie os estados de acordo com o esperado ou com o que ocorre durante o estado. Lembre-se de que um estado não é um 'momento'; é um período durante o qual a máquina de estado aguarda que algo aconteça. Por exemplo, 'waitingForEnd' é um nome melhor do que 'end'; 'timingSomeTask' é melhor do que 'timeout'. Não nomeie estados como se eles fossem ações.
  • Nomeie todos os estados e as transições dentro de uma máquina de estado com exclusividade; isso facilitará a depuração no nível da origem.
  • Use variáveis de estado (atributos usados para controlar o comportamento) com cautela; não as utilize para evitar a criação de novos estados. Onde houver poucos estados, com pouco ou nenhum comportamento dependente do estado, e onde houver pouco ou nenhum comportamento que possa ser simultâneo ou independente do objeto que contém a máquina de estado, as variáveis de estado poderão ser usadas. Se houver um comportamento complexo e dependente do estado que seja simultâneo, ou se os eventos que devem ser manipulados puderem ser originados fora do objeto que contém a máquina de estado, convém usar uma colaboração de dois ou mais objetos ativos (possivelmente definidos como uma composição). Em sistemas em tempo real, o comportamento simultâneo complexo  dependente do estado deve ser modelado utilizando uma cápsula que contém subcápsulas.
  • Se houver mais de 5 ± 2 estados em um único diagrama, considere o uso de subestados. O bom senso se aplica: dez estados em um padrão absolutamente regular pode ser o ideal, mas dois estados com quarenta transições entre eles obviamente precisam ser reavaliados. Verifique se a máquina de estado é compreensível.
  • Nomeie as transições pelo que dispara o evento e/ou pelo que ocorre durante a transição. Escolha nomes que melhorem a compreensão.
  • Ao encontrar um vértice de opção, você deve se perguntar se poderá delegar a responsabilidade por aquela opção para outro componente, para que ela seja apresentada ao objeto como um conjunto distinto de sinais sobre os quais agir (por exemplo, no lugar de uma opção em msg->dados > x), delegar a decisão ao emissor ou a outro agente intermediário e enviar um sinal com a decisão explícita no nome do sinal (por exemplo, utilize sinais denominados isFull e isEmpty no lugar de ter um sinal denominado value e verifique os dados da mensagem).
  • Nomeie de forma descritiva a pergunta respondida no vértice de opção, por exemplo, 'isThereStillLife' ou 'isItTimeToComplain'.
  • Dentro de um objeto qualquer, procure manter nomes exclusivos para os vértices de opção (pelo mesmo motivo de manter nomes exclusivos para os nomes de transição).
  • Existem fragmentos de código excessivamente longos nas transições? Devem ser usadas funções em seu lugar? Os fragmentos de código comuns são capturados como funções? Uma transição deve ser lida como um pseudocódigo de alto nível e deve estar de acordo com as mesmas, ou mesmo mais rigorosas, regras de extensão das funções C++. Por exemplo, uma transição com mais de 25 linhas de código é considerada excessivamente longa.
  • As funções devem ser nomeadas pelo que elas fazem.
  • Preste bastante atenção às ações de entrada e de saída: é muito fácil fazer mudanças e esquecer de alterar as ações de entrada e de saída.
  • As ações de saída podem ser usadas para fornecer características de segurança (por exemplo, a ação de saída do estado 'heaterOn' desliga o aquecedor) onde as ações são usadas para impor uma declaração.
  • Geralmente, os subestados devem conter dois ou mais estados, a menos que a máquina de estado seja abstrata e será refinada por subclasses do elemento confinado.
  • Pontos de opção devem ser usados no lugar de lógica condicional em ações ou transições. Os pontos de opção são facilmente vistos, ao passo que a lógica condicional no código está oculta na visualização e é fácil não notá-la.
  • Evite as condições de guarda.
    • Se o evento disparar várias transições, não haverá controle sobre qual condição de guarda será avaliada primeiro. Como conseqüência, os resultados podem ser imprevisíveis.
    • Mais de uma condição de guarda pode ser 'verdadeira', mas apenas uma transição poderá ser seguida. O caminho escolhido pode ser imprevisível.
    • As condições de guarda não são visuais, é mais difícil 'ver' a presença delas.
  • Evite máquinas de estado que pareçam fluxogramas.
    • Isso pode indicar uma tentativa de modelar uma abstração que não está realmente lá, como:
      • utilizar uma classe ativa para modelar o comportamento mais adequado para uma classe passiva ou de dados
      • modelar uma classe de dados usando uma classe de dados e uma classe ativa que estão acopladas diretamente (isto é, a classe de dados foi usada para repassar informações de tipo, mas a classe ativa contém a maioria dos dados que devem ser associados à classe de dados).
    • Esse uso equivocado de máquinas de estado pode ser reconhecido pelos seguintes sintomas:
      • mensagens enviadas para 'si', principalmente apenas para reutilizar o código
      • poucos estados, com muitos pontos de opção
      • em alguns casos, uma máquina de estado sem ciclos. Essas máquinas de estado são válidas em aplicativos de controle de processo ou na tentativa de controlar uma seqüência de eventos. Geralmente, a presença delas durante a análise representa a degeneração da máquina de estado em um fluxograma.
    • Quando o problema for identificado:
      • Convém dividir a classe ativa em unidades menores com responsabilidades mais definidas,
      • Mova mais comportamento para dentro de uma classe de dados que esteja associada à classe ativa do problema.
      • Mova mais comportamento para dentro das funções de classe ativa.
      • Faça sinais mais significativos em vez de confiar nos dados.

Projetando com Máquinas de Estado Abstratas

Uma máquina de estado abstrato é uma máquina de estado na qual é necessário incluir mais detalhes para que possa ser utilizada para finalidades práticas. As máquinas de estado abstrato podem ser utilizadas para definir comportamento genérico e reutilizável que será mais refinado nos elementos de modelo subseqüentes.

Diagrama descrito na legenda.

Figura 5. Uma máquina de estado abstrato.

Considere a máquina de estado abstrato na Figura 5. A máquina de estado simples retratada representa o nível mais abstrato de comportamento (o autômato de "controle") de diferentes tipos de elementos em sistemas direcionados a eventos. Embora eles compartilhem essa forma de alto nível, os diferentes tipos de elementos podem ter comportamentos bem diversos no estado Execução dependendo das suas finalidades. Portanto, essa máquina de estado seria melhor definida em alguma classe abstrata que servisse como a classe original para as diversas classes ativas especializadas.

Vamos, então, definir dois refinamentos diferentes dessa máquina de estado abstrato, usando herança. Esses dois refinamentos, R1 e R2, são mostrados na Figura 6. Para maior clareza, desenhamos os elementos herdados da classe pai usando uma caneta cinza.

Diagrama descrito na legenda.

Figura 6. Dois refinamentos da máquina de estado na Figura 5.

Os dois refinamentos diferem claramente no modo como decompõem o estado Em Execução e em como estendem a transição "inicial" original. Essas opções só podem ser feitas quando o refinamento for conhecido e, portanto, não poderiam ter sido feitas com uma única transição de extremidade-a-extremidade na classe abstrata.

Estados de Cadeia

A capacidade de "continuar" tanto as transições de entrada como as de saída é fundamental para o tipo de refinamento descrito acima. Pode parecer que os pontos de entrada e os estados finais, combinados com as transições de continuação, sejam suficientes para fornecer essa semântica. Mas isso não será suficiente quando houver várias transições diferentes que precisam ser estendidas.

O que é necessário para o padrão de comportamento abstrato é um modo de encadear dois ou mais segmentos de transição que sejam executados no escopo de uma única etapa indivisível. Isso significa que as transições que entram no estado hierárquico são divididas na parte de entrada que realmente encerra na fronteira do estado e na extensão que continua dentro do estado. Da mesma forma, as transições de saída que procedem de um estado hierarquicamente aninhado são segmentadas em uma parte que encerra na fronteira do estado confinado e outra parte que prossegue da fronteira do estado para o estado de destino. Esse efeito pode ser conseguido em UML com a introdução do conceito de estado de cadeia. Isso é modelado por um estereótipo (<<chainState>>) do conceito de Estado da UML. Trata-se de um estado cuja única finalidade é "encadear" mais transições automáticas (sem acionadores) em uma transição de entrada. Um estado em cadeia não possui estrutura interna - nenhuma ação de entrada, nenhuma tarefa interna, nenhuma ação de saída. Também não possui transições disparadas por eventos. Ele pode ter inúmeras transições de entrada. Ele pode ter uma transição de saída sem um evento trigger; essa transição é acionada quando uma transição de entrada ativa o estado. A finalidade do estado é encadear uma transição de entrada a uma transição de saída separada. Entre as transições de entrada e a transição de saída encadeada, uma se conecta a outro estado dentro do estado armazenador e a outra de conecta a outro estado fora do estado armazenador. A finalidade de introduzir um estado em cadeia é separar a especificação interna do estado armazenador do seu ambiente externo. É uma questão de encapsulamento.

Na verdade, um estado em cadeia representa um estado de "passagem" que serve para encadear uma transição a uma transição específica de continuação. Se nenhuma transição de continuação for definida, a transição encerrará no estado em cadeia, e alguma transição em um estado confinado deverá, por fim, acionar a movimentação.

O exemplo de segmento de máquina de estado na Figura 7 ilustra estados em cadeia e suas notações. Os estados em cadeia são representados em um diagrama de máquina de estado por pequenos círculos brancos localizados dentro do estado hierárquico apropriado (essa notação é semelhante aos estados inicial e final, com os quais se parece). Os círculos são ícones do estereótipo do estado em cadeia e, em geral, são desenhados próximos à fronteira por conveniência. (Na verdade, uma variação de notação seria desenhá-los na borda do estado confinado.)

Diagrama descrito no texto associado.

Figura 7. Estados em cadeia e transições encadeadas.

A transição encadeada neste exemplo consiste em três segmentos de transição encadeada e1/a11-/a12-/a13. Quando o sinal e1 é recebido, ocorre a transição rotulada e1/a11, sua ação a11 é executada, e o estado em cadeia c1 é alcançado. Em seguida, ocorre a transição de continuação entre c1 e c2 e, por fim, como c2 também é um estado em cadeia, ocorre a transição de c2 para S21. Se todos os estados ao longo desses caminhos tiverem ações de entrada e de saída, a seqüência real de ações executadas é seguida da seguinte forma:

  • ação de saída de S11
  • ação a11
  • ação de saída de S1
  • ação a12
  • ação de entrada de S2
  • ação a13
  • ação de entrada de S21

Tudo isso é executado no escopo de um único passo indivisível.

Isso deve ser comparado com a semântica de execução de ação da transição direta e2/a2, que é:

  • ação de saída de S11
  • ação de saída de S1
  • ação a2
  • ação de entrada para estado S2
  • ação de entrada para estado S21