aluno: Leandro Lima
ano/sem: 2008/2o.
data do laboratório (num. da semana) : 12/10/08 (3)
Introdução
Nesse 3° Laboratório de CES-22, criou-se uma biblioteca de máquina de estados finita e determinística movida a eventos. Através dela, pode-se criar máquinas que implementem todas as suas funcionalidades em diversas situações. Visando adequar as particularidades da máquina aos conteúdos da disciplina vistos em aula, foram utilizados os padrões Observer e Command, além de classes anônimas.
Desenvolvimento
Para implementar a máquina de estados referida, colocamos todas as classes em um único pacote. Contudo, em programas com nível de complexidade maior é interessante que haja uma divisão das classes considerando seus principais aspectos, escopos de atuação, nível de acoplamento e semântica.
Diagramas de Classes
Aqui podemos ver como ficou o Diagrama de Classes do programa:
Classe StateMachine
Essa é a principal classe de nosso programa, funcionando como uma interface pública. Como variáveis de instância, além do estado atual, foram utilizadas ArrayLists para armazenar os estados, os eventos e os observadores. Isso foi feito com o intuito de utilizar uma abordagem mais simplificada. Alguns métodos adicionais foram implementados para prover recursos, notadamente aos tipos de retorno desejados. Os estados, juntamente com os observadores e os eventos, foram armazenados em ArrayLists. Essa abordagem tem uma desvantagem: a de ter que armazenar em cada célula da List um terno (event, source e destination). Desse modo, percebe-se que um evento pode estar associado a mais de uma célula. Isso foi resolvido pelo método findEventByName e com seu método auxiliar doesStateExist. Alternativamente, poderíamos contar com uma lista auxiliar para armazenar todas as transições.
package stateMachine; import java.util.ArrayList; public class StateMachine { private State currentState; private ArrayList<State> states = new ArrayList<State>(); private ArrayList<Event> events = new ArrayList<Event>(); private ArrayList<Observer> observers = new ArrayList<Observer>(); //@Override public void appendTransitionToEvent(String event, String fromState, String toState) { Event eventoNovo = new Event(event, findStateByName(fromState), findStateByName(toState)); this.events.add(eventoNovo); } //@Override public void appendTransitionToEvent(String event, String[] fromState, String toState) { for(int i = 0; i < fromState.length; i++){ Event eventoNovo = new Event(event, findStateByName(fromState[i]), findStateByName(toState)); this.events.add(eventoNovo); } } //@Override public void createEvent(String name, String[] from, String to, EventCommand onSuccess) { for(int i = 0; i < from.length; i++){ Event eventoNovo = new Event(name,findStateByName(from[i]), findStateByName(to), onSuccess); this.events.add(eventoNovo); } } //@Override public void createEvent(String name, String[] from, String to) { for(int i = 0; i < from.length; i++){ Event eventoNovo = new Event(name,findStateByName(from[i]),findStateByName(to)); this.events.add(eventoNovo); } } //@Override public void createEvent(String name, String from, String to, EventCommand onSuccess) { Event eventoNovo = new Event(name,findStateByName(from), findStateByName(to), onSuccess); this.events.add(eventoNovo); } //@Override public void createEvent(String name, String from, String to) { Event eventoNovo = new Event(name,findStateByName(from),findStateByName(to)); this.events.add(eventoNovo); } //@Override public void createState(String name, StateCommand onEnter) { State state = new State(name, onEnter); states.add(state); } //@Override public void createState(String name) { State state = new State(name, null); states.add(state); } //@Override public boolean fireEvent(String name) throws UndefinedTransitionException { Event e = findEventByName(name); if (e.getEventGuard() != null) if (e.getEventGuard().shouldPerform() == false) return false; this.currentState = e.getDestinationState(); e.executeCommand(); notifyObservers(); this.currentState.executeCommand(); return true; } private Event findEventByName(String name) throws UndefinedEventException, UndefinedStateException, UndefinedTransitionException { Boolean foundEvent = false; for(int i = 0; i < events.size(); i++) if (events.get(i).getName().equals(name)){ if (!doesStateExists(events.get(i).getDestinationState())) //|| !doesStateExists(events.get(i).getSourceState()) throw new UndefinedStateException(); if (events.get(i).getSourceState().equals(this.currentState)) return events.get(i); foundEvent = true; } if (foundEvent) throw new UndefinedTransitionException(); throw new UndefinedEventException(); } . . . //@Override public void setGuardToEvent(String event, EventGuard g) { for(int i = 0; i < events.size(); i++) if (events.get(i).getName().equals(event)) events.get(i).setEventGuard(g); } //@Override public void setInitialState(String name) { this.currentState = findStateByName(name); } private State findStateByName(String name) { //throws UndefinedStateException { for(int i = 0; i < states.size(); i++) if (states.get(i).getName().equals(name)) return states.get(i); return null; //throw new UndefinedStateException(); } private Boolean doesStateExists(State state) { for(int i = 0; i < states.size(); i++) if (states.get(i).equals(state)) return true; return false; } }
Classe State e Observer
Classe State criada para concretizar os atributos de estado e viabilizar a execução de StateCommand. Os observadores são registrados pela própria máquina de estados para serem atualizados de seu estado atual e com isso realizar as modificações necessárias (método update). O conceito de classes anônimas foi explorado aqui e foi muito útil, pois um determinado método (StateCommand - método de chegada no estado) já é executado no momento de sua declaração (um objeto é passado como parâmetro).
package stateMachine; public class State { private String name; private StateCommand command; public State(String name, StateCommand command) { this.name = name; this.command = command; } public String getName() { return name; } public void executeCommand() { if (this.command == null) return; command.execute(); } }
Classes de Exceções (UndefinedEventException, UndefinedStateException e UndefinedTransitionException)
Essas classes foram criadas como subclasses de Exception e visam lançar uma exceção se algo de errado ocorreu no programa (as de Evento e Estado são avaliadas em tempo de execução). Vale observar que o método findEventByName foi criado para lançar 2 exceções (Event e Transition), contudo foi escolhida uma implementação para lançar as 3 exceções, cada qual em uma situação diferente. A exceção de Estado, por exemplo, é lançada se o estado de destino não existe, porém uma outra alternativa seria também verificar se a origem não existe (esse comando está comentado no código de StateMachine).
package stateMachine; public class UndefinedEventException extends RuntimeException { }
package stateMachine; public class UndefinedStateException extends RuntimeException { }
package stateMachine; public class UndefinedTransitionException extends Exception { }
Classe de Teste (StateMachineTest)
O teste ShouldReturnErrorIfEventDoesntExist durante uma das implementações intermediárias do programa estava verificando no createEvent se os estados existiam e lançando Exceção de Transição, mas os testes só estavam dando erro em Exceções de Evento (não existia o SourceState, mas existia o evento). Daí, a escolha por lançar mais de uma exceção. O comando catch usado no teste independe da ordem que é usado (o comando captura de qualquer índice da pilha de exceções geradas). Em testes adicionais, foi visto que o resultado se mantém o mesmo ao trocar a ordem dos catchs.
O nome de UndefinedEventError foi alterado para UndefinedEventException para manter a consistência dos testes.
Classes de Evento (EventGuard, EventCommand e Event)
As exceções lançadas em findEventByName (comentadas na seção de exceções) são justificadas pois uma das etapas do teste deixava o evento ser criados mesmo que não existisse a transição para um estado válido. A exceção seria lançada somente ao se disparar esse evento.
A classe Event também foi criada pois serviria de veículo do EventGuard com o EventCommand, ocupando células na ArrayList de Eventos. Cada evento está associado a um evento de origem e um de destino específico. Como visto, as ações e condições de guarda foram definidas, com o uso de classes anônimas, quando se chamam os métodos que criam os eventos e estados.
package stateMachine; public class Event { private String name; private State sourceState, destinationState; private EventCommand command; private EventGuard eventGuard; public Event(String name, State sourceState, State destinationState) { super(); this.name = name; this.sourceState = sourceState; this.destinationState = destinationState; this.command = null; this.eventGuard = null; } public Event(String name, State sourceState, State destinationState, EventCommand command) { super(); this.name = name; this.sourceState = sourceState; this.destinationState = destinationState; this.command = command; } public void executeCommand(){ if (command == null) return; this.command.execute(this.sourceState.getName()); } . . . }
Exemplo de Aplicação (Dado em Sala)
Nesse exemplo podemos enxergar o Diagrama de Classes:
Para enxergamos a aplicação da máquina de estados, iremos percorrer os estados numa certa ordem e avaliando as respostas do programa. O comportamento da turma vai sendo observado pelo professor e, dependendo de qual seja, ele muda seu estado também.
Inicialmente, o professor encontra-se "motivado" e a turma "dormindo".
Agora a turma dispara "usar_note":
Note que a correcao da prova vai mudando tambem.
Agora a turma tenta motivar o professor:
Agora ele volta a ficar "motivado":
A turma dispara "dormir":
O professor volta a ficar "desmotivado".
Contudo, a turma dispara "usar_note":
Agora o professor está "irado". Ele nunca mais será o mesmo. Repare que a correção da prova também vai mudando.
Se a turma quiser prestar atenção…
…ele já estará "vacinado".
Agora o professor está recebendo milhões de dólares.
Mesmo que a turma tente desmotivá-lo, isso não muda seu estado para "irado" (o mesmo funciona de "motivado" para "desmotivado").
Uma mostra da evolução da resposta ao longo do tempo pode ser observado no Terminal exibido a seguir:
Exemplo de Aplicação (Novo)
O novo exemplo de aplicação consiste no seguinte fluxo de estados:
As alterações ente os estados são dadas pelo eventos: Trabalhar, Vadiar e Mega-Sena. No exemplo anterior foi feita uma execução passo-a-passo, contudo nesse exemplo as funcionalidades serão implementadas diretamente numa classe Main.
Classe Main
O estado inicial varia aleatoriamente entre "aluno e "vagabundo".
package ExemploNovo; public class Main { public static void main(String[] args) { Individuo pessoa = new Individuo(); System.out.println("Olá! Meu estado inicial é: " + pessoa.getState()); if (pessoa.getState() == "aluno"){ System.out.println("\n(Vadiando)\n"); pessoa.fire("vadiar"); System.out.println("Meu estado atual é: " + pessoa.getState()); System.out.println(pessoa.salario()); System.out.println("\n(Trabalhando)\n"); pessoa.fire("trabalhar"); System.out.println("Meu estado atual é: " + pessoa.getState()); System.out.println(pessoa.salario()); System.out.println("\n(Trabalhando)\n"); pessoa.fire("trabalhar"); System.out.println("Meu estado atual é: " + pessoa.getState()); System.out.println(pessoa.salario()); System.out.println("\n(Trabalhando)\n"); pessoa.fire("trabalhar"); System.out.println("Meu estado atual é: " + pessoa.getState()); System.out.println(pessoa.salario()); System.out.println("\n(Trabalhando)\n"); pessoa.fire("trabalhar"); System.out.println("Meu estado atual é: " + pessoa.getState()); System.out.println(pessoa.salario()); System.out.println("\n(Vadiando)\n"); pessoa.fire("vadiar"); System.out.println("Meu estado atual é: " + pessoa.getState()); System.out.println(pessoa.salario()); } else { System.out.println("\n(Vadiando)\n"); pessoa.fire("vadiar"); System.out.println("Meu estado atual é: " + pessoa.getState()); System.out.println(pessoa.salario()); System.out.println("\n(Trabalhando)\n"); pessoa.fire("trabalhar"); System.out.println("Meu estado atual é: " + pessoa.getState()); System.out.println(pessoa.salario()); System.out.println("\n(Apostando)\n"); pessoa.fire("mega-sena"); System.out.println("Meu estado atual é: " + pessoa.getState()); System.out.println(pessoa.salario()); System.out.println("\n(Trabalhando)\n"); pessoa.fire("trabalhar"); System.out.println("Meu estado atual é: " + pessoa.getState()); System.out.println(pessoa.salario()); System.out.println("\n(Vadiando)\n"); pessoa.fire("vadiar"); System.out.println("Meu estado atual é: " + pessoa.getState()); System.out.println(pessoa.salario()); } } }
Classe Individuo
package ExemploNovo; public class Individuo extends WithSM { public Individuo() { super(); setUpSM(); } public String salario() { if(getState() == "aluno" || getState() == "vagabundo") return "Eita vida dificil... to ganhando nada."; else if(getState() == "esperancoso") return "To juntando as micharias pra fazer uma fezinha."; else if(getState() == "trainee") return "Opa! Já dá pra levar a namorada pro cinema!"; else if (getState() == "funcionario") return "Agora, finalmente compro meu carrinho."; else if (getState() == "gerente") return "Férias = Viagens!!"; else return "HAHAHA!! Vivo num mundo sem limites!!"; } private void setUpSM() { //criacao dos estados sm.createState("aluno"); sm.createState("trainee"); sm.createState("funcionario"); sm.createState("gerente"); sm.createState("vagabundo"); sm.createState("esperancoso"); sm.createState("rico", new stateMachine.StateCommand(sm) { public void execute() { System.out.println("Estou rico!!"); } } ); //definido o estado inicial int a = (int) (Math.random()*2); if (a == 0) sm.setInitialState("vagabundo"); else sm.setInitialState("aluno"); //evento trabalhar sm.createEvent("trabalhar", "rico", "rico", new stateMachine.EventCommand(sm) { public void execute(String previousState) { System.out.println("Como serei ainda mais rico?? HAHA!"); } }); sm.appendTransitionToEvent("trabalhar", "gerente", "rico"); sm.appendTransitionToEvent("trabalhar", "aluno", "trainee"); sm.appendTransitionToEvent("trabalhar", "trainee", "funcionario"); sm.appendTransitionToEvent("trabalhar", "funcionario", "gerente"); sm.appendTransitionToEvent("trabalhar", "vagabundo", "esperancoso"); //evento vadiar sm.createEvent("vadiar", "vagabundo", "vagabundo", new stateMachine.EventCommand(sm) { public void execute(String previousState) { System.out.println("É o fundo do poço!"); } }); sm.createEvent("vadiar", "rico", "rico", new stateMachine.EventCommand(sm) { public void execute(String previousState) { System.out.println("Nunca deixarei de ser rico!"); } }); sm.appendTransitionToEvent("vadiar", "aluno", "aluno"); sm.appendTransitionToEvent("vadiar", "esperancoso", "vagabundo"); sm.appendTransitionToEvent("vadiar", "trainee", "aluno"); sm.appendTransitionToEvent("vadiar", "funcionario", "trainee"); sm.appendTransitionToEvent("vadiar", "gerente", "funcionario"); //evento mega-sena sm.createEvent("mega-sena", "esperancoso", "rico", new stateMachine.EventCommand(sm) { public void execute(String previousState) { System.out.println("Ganhei na loteria!"); } }); } }
Classe WithSM
package ExemploNovo; import stateMachine.*; public class WithSM { protected StateMachine sm; public WithSM() { sm = new StateMachine(); } public void fire(String eventName){ try{ sm.fireEvent(eventName); } catch(UndefinedTransitionException e) { System.out.println("ERRO: transição não definida: " + e.getMessage()); } } public String getState(){ return sm.getCurrentStateName(); } }
Terminal do BlueJ
Conclusão
Como descrito ao longo do relatório, foram utilizadas implementações intermediárias nesse programa que atendiam às especificações, porém não passava nos testes. Isso comprova o alto nível de complexidade da prática e nos ensina que, ao executar projetos desse tipo, temos que sempre seguir um desenvolvimento orientado a testes (as criações dos métodos são realizadas gradualmente, conforme se vai passando nos testes), como foi realizado. Se não fosse isso, a implementação seria bem mais difícil, pois grandes blocos de código teriam que ser avaliados de uma só vez.
Durante o desenvolvimento do programa, procurou-se criar as classes objetivando um baixo nivel de acoplamento, porém, por optar por estruturas de dados mais simples para armazenar eventos e estados, isso foi algo conseguido com certa dificuldade.
Uma melhoria sugerida para o projeto seria a de incluir no próprio teste de unidade métodos que verificassem o funcionamento da interface Observer e os métodos de appendTransitionToEvent, que foram explorados apenas no exemplo. Em uma versão intermediária, o programa estava executando o StateCommand antes de mudar seu estado atual. Isso não foi coberto pelos testes e poderia ser explorado.
A criação de uma biblioteca de máquina de estados é extremamente útil, pois pode ser utilizada por outros programas que necessitem de um grafo de fluxo de estados e interações de eventos, como jogos (inclusive o do projeto do exame).





