aluno: Bruno César Alves
ano/sem: 2008/2o.
data do laboratório (num. da semana) : 06/08/2008 (2)
Introdução
Este relatório refere-se a construção de uma máquina de estados determinista, entendo como determinista qualquer transição que tenha uma única combinação entrada e saída. Minha máquina de estados permite a saída de um estado para vários, entretanto cada saída está relacionada a um evento diferente.
Além da máquina de estados, foi construida uma aplicação que a utiliza para definir o status de um personagem de um game.
Desenvolvimento
Nas instruções do relatório, foi citada a criação das classes UndefinedEventException e UndefinedStateException, porém, para não causar erro nos testes, tomei a liberdade de alterar o nome dessas classes para UndefinedEventError e UndefinedStateError.
Segue a máquina de estados criada no blueJ:
De forma geral, o papel da classe StateMachine é realizar todas as requisições do usuário da máquina, esta classe guarda as principais informações sobre a máquina e realiza todos os links do seu usuário com o resto do pacote, ela utiliza o padrão observer e deve guardar, além dos estados e eventos possíveis, todos os seus observadores. Os métodos que criam eventos e estados usam overload para diferentes tipos de criação. Os métodos setInitialState e setCurrentState diferem, pois o segundo executa o commando de estado.
package stateMachine; import java.util.Vector; public class StateMachine { //variáveis private Vector <State> states = new Vector(); //vetor que armazena todos os estados da máquina private Vector <Event> events = new Vector(); //vetor que armazena todos os eventos da máquina private Vector <Observer> observers = new Vector(); //vetor que armazena todos os observadores da máquina private State currentState; //guarda o estado atual da máquina private Observer observer; //variável usada para guardar um observer usado em algum processo //add um observador que deseja observar a máquina public void registerObserver(Observer observer){ observers.add(observer); } //usada para atualizar os observadores protected void notifyObservers(){ for(Observer obs : observers){ obs.update(this, currentState.getName()); } } //métodos para a definição da máquina //criaum estado com um comando de estado public void createState(String name, StateCommand onEnter){ states.add(new State(this,name, onEnter)); } //criaum estado sem um comando de estado public void createState(String name){ states.add(new State(this,name)); } //define um estado incial para a máquina public void setInitialState(String name){ for(State state_ : states){ if(name.equals(state_.getName())) currentState=state_; } } //cria um evento e transições de varios estados para um estado com um comando de evento public void createEvent(String name, String[] from, String to, EventCommand onSuccess){ events.add(new Event(this, name,from,to, onSuccess)); } //cria um evento e transições de varios estados para um estado sem comando de evento public void createEvent(String name, String[] from, String to){ events.add(new Event(this, name,from,to)); } //cria um evento de um estado para outro com comando de evento public void createEvent(String name, String from, String to, EventCommand onSuccess){ events.add(new Event(this, name,from,to, onSuccess)); } //cria um evento de um estado para o outro sem comando de evento public void createEvent(String name, String from, String to){ events.add(new Event(this, name,from,to)); } //cria uma transição de um estado para outro em um evento já existente public void appendTransitionToEvent(String event, String fromState, String toState){ for(Event event_ : events){ if(event.equals(event_.getName())) event_.AppendTransition(fromState, toState); } } //cria varias transições de estados para um estado em um evento já existente public void appendTransitionToEvent(String event, String[] fromState, String toState){ for(Event event_ : events){ if(event.equals(event_.getName())){ for(String fromState_ : fromState) event_.AppendTransition(fromState_,toState); } } } //cria uma guarda para limitar a execução d eum determinado evento public void setGuardToEvent(String event,EventGuard g){ for( Event event_ : events){ if(event.equals(event_.getName())) event_.setEventGuard(g); } } // métodos para a execução da máquina //executa um evento da máquina de estados //os exceptions evitam que eventos, estados ou transições inxistentes sejam realizadas public boolean fireEvent(String name) throws UndefinedTransitionException{ for(Event event_ : events){ if(name.equals(event_.getName())){ event_.fireTransition(this); return true; } } throw new UndefinedEventError(); } //retorna o estado atual da máquina public String getCurrentStateName(){ return this.currentState.getName(); } //define um novo estado para a máquina executando o comando de estado public boolean setCurrentState(String newState){ for( State state_ : states){ if(newState.equals(state_.getName())){ currentState = state_; state_.fireCommand(); return true; } } System.out.println("O estado " + newState + "não existe"); return false; } }
A classe State instancia os estados da máquina, seu papel é guardar as variáveis do estado, bem como seu comando de estado, que é definido como vazio na construção da instância caso não seja solicidado.
package stateMachine; public class State { private String name; //guarda o nome do estado private StateCommand sc; //guarda o comando que deve ser executado quando a variável chega em um estado private StateMachine sm; //guarda a máquina de estado a qual o estado pertence //contrutor que define a maquina a qual pertence e o nome do estado public State(StateMachine sm, String name) { this.sm = sm; this.name = name; setInitialStateCommand(); } //contrutor que define a maquina a qual pertence, o nome do estado e o comando de estado public State(StateMachine sm, String name, StateCommand sc) { this.sm = sm; this.name = name; setStateCommand(sc); } //define o comando de estado public void setStateCommand(StateCommand sc) { this.sc = sc; } //define um comando de estado vazio public void setInitialStateCommand() { sc = new StateCommand(sm) { public void execute() { ; } }; } //retorna o nome do estado public String getName() { return this.name; } //executa o comando de estado public void fireCommand() { sc.execute(); } }
A classe Event instância os eventos da máquina, como na classe State, existe mais de um construtor. Os métodos setNoGuard e setNoCommand auxiliam no trabalho com as respectivas classes anônimas no caso em que elas não são solicitadas pelo usuário. Para que a determinação seja mantida, na criação de transições dentro do evento, o método verifyTransition procura por uma transição que já tenha o estado da nova transição.
package stateMachine; import java.util.Vector; public class Event { private Vector<EventTransition> transitions = new Vector<EventTransition>(); //armazena todos as transições realizadas pelo evento private String name; //guarda o nome do evento private EventCommand ec; //armazena o comando de evento private EventGuard eg; //armazena a guarda do evento private StateMachine sm; //armazena a máquina de estados a qual o evento pertence //construtor que isntancia um evento e uma transição public Event(StateMachine sm,String name, String from, String to) { this.sm = sm; this.name = name; transitions.add(new EventTransition(this,from,to)); setNoGuard(); setNoCommand(); } //construtor que isntancia um evento e varias transições public Event(StateMachine sm,String name, String[] from, String to) { this.sm = sm; this.name = name; for( String from_ : from){ if(verifyTransition(from_,to)) transitions.add(new EventTransition(this,from_,to)); } setNoGuard(); setNoCommand(); } //construtor que isntancia um evento e uma transição com um comando de evento public Event(StateMachine sm,String name, String from, String to, EventCommand ec) { this.sm = sm; this.name = name; this.ec =ec; if(verifyTransition(from,to)) transitions.add(new EventTransition(this,from,to)); setNoGuard(); } //construtor que isntancia um evento e varias transições com um comando de evento public Event(StateMachine sm,String name, String[] from, String to, EventCommand ec) { this.sm = sm; this.name = name; this.ec = ec; for( String from_ : from){ if(verifyTransition(from_,to)) transitions.add(new EventTransition(this,from_,to)); } setNoGuard(); } //cria uma nova transição para o evento public void AppendTransition(String fromState, String toState){ if(verifyTransition(fromState,toState)) transitions.add(new EventTransition(this,fromState,toState)); } //define um comando de evento para o evento public void setEventCommand(EventCommand ec) { this.ec = ec; } //define uma guarda para o evento public void setEventGuard(EventGuard eg) { this.eg = eg; } //deixa o vento sem guarda public void setNoGuard() { this.eg = new EventGuard() { public boolean shouldPerform() { return true; } }; } //deixa o evento sem comando de evento public void setNoCommand() { this.ec = new EventCommand(sm) { public void execute(String previousState) { } }; } //retorna a guarda do evento public EventGuard getEventGuard() { return eg; } //retorna o nome do evento public String getName() { return this.name; } //executa a transição do evento public boolean fireTransition(StateMachine sm) throws UndefinedTransitionException { for( EventTransition et : transitions){ if(et.getFrom().equals(sm.getCurrentStateName())&eg.shouldPerform()){ if(!sm.setCurrentState(et.getTo())) throw new UndefinedStateError(); sm.notifyObservers(); ec.execute(et.getTo()); return true; } } throw new UndefinedTransitionException(); } //verifica se existe alguma transição que parte do estado: StateFrom. //essa verificação é importante para que nao ocorram transição duplas para um mesmo evento public boolean verifyTransition(String stateFrom,String stateTo){ for(EventTransition transition_ : transitions){ if(transition_.getFrom().equals(stateFrom)) { System.out.println("A criação da transição: \"" + stateFrom + "\" para \"" + stateTo + "\" foi impedida pois causaria indeterminação na máquina"); return false; } } return true; } }
A classe EventTransition se refere à transição de um estado para o outro, sua função é armazenar o estado de saida e de chegada de uma transição.
package stateMachine; public class EventTransition { private String from; //guarda o estado de saida da transição private String to; //guarda o estado de chegada da transição private Event event; //guarda o evento ao qual a transição pertence //construtor public EventTransition(Event event, String from, String to) { this.from =from; this.to =to; this.event = event; } //retorna o estado de saida da transição public String getFrom(){ return from; } //retorna o estado de chegada da transição public String getTo(){ return to; } }
Teste da máquina de estados
A máquina de estados foi testada com sucesso, segue o resultado obtido:
Aplicação
A aplicação desenvolvida tem como objetivo controlar um personagem de um game, de forma que seus estados estao sob influência de um usuário e do proprio game.
Segue a aplicação feita no blueJ:
A classe Goblin sobreescreve a classe StateMachine, sua função é configurar a máquina de estados para o personagem goblin.
import stateMachine.*; public class Goblin implements stateMachine.Observer { protected StateMachine sm; protected String state; protected boolean macho; //contrutor public Goblin() { sm = new StateMachine(); setUpSM(); sm.registerObserver(this); } //dispara o evento public void fire(String eventName){ try{ sm.fireEvent(eventName); } catch(UndefinedTransitionException e) { System.out.println("ERRO: trasição não definida: " + e.getMessage()); } } //retorna o estado atual public String getState(){ return sm.getCurrentStateName(); } //configura a maquiana de estados para os estados e eventos do goblin private void setUpSM() { //criando estados sm.createState("parado", new stateMachine.StateCommand(sm) { public void execute() { System.out.println("Estou parado"); } } ); sm.createState("correndo", new stateMachine.StateCommand(sm) { public void execute() { System.out.println("Estou correndo"); } } ); sm.createState("caido", new stateMachine.StateCommand(sm) { public void execute() { System.out.println("Estou caido"); } } ); sm.createState("lasagra", new stateMachine.StateCommand(sm) { public void execute() { System.out.println("Rodolpho Castro"); } } ); sm.createState("jandaia", new stateMachine.StateCommand(sm) { public void execute() { System.out.println("O que?"); } } ); sm.createState("boladao", new stateMachine.StateCommand(sm) { public void execute() { System.out.println("So many Goblins!!!!!"); } } ); sm.setInitialState("parado"); String[] from1 = {"parado", "correndo","caido","lasagra","boladao"}; sm.createEvent("fritar", from1, "jandaia", new stateMachine.EventCommand(sm) { public void execute(String previousState) { System.out.println("momento altista..."); } }); String[] from2 = {"parado", "correndo","caido","jandaia","boladao"}; sm.createEvent("negao", from2, "lasagra", new stateMachine.EventCommand(sm) { public void execute(String previousState) { System.out.println("eeeeeeepa..."); } }); String[] from3 = {"correndo","caido","lasagra","jandaia","boladao"}; sm.createEvent("parar", from3, "parado"); String[] from4 = {"parado","lasagra","jandaia","boladao"}; sm.createEvent("correr", from4, "correndo"); String[] from5 = {"parado", "correndo","lasagra","jandaia","boladao"}; sm.createEvent("cair", from5, "caido"); String[] from6 = {"parado", "correndo","caido","lasagra","jandaia"}; sm.createEvent("bolar", from6, "boladao"); sm.setGuardToEvent("negao", new stateMachine.EventGuard() { public boolean shouldPerform(){ return !isMutchoMatcho(); //nunca fica lasagra se eh macho } }); } //retorna se o goblin eh mutchomatcho protected boolean isMutchoMatcho(){ return macho; } //coloca um novo valor para a variavel macho protected void setMacho(String is){ if(is.equals("sim")) macho = true; else macho = false; } //faz alteração de acordo com o update dado na clase observer da maquina de estados public void update(StateMachine sm, String newStateName){ state= newStateName; } }
A classe usuario tem como função receber inputs do teclado do usuario e executar eventos que alteram o estado do goblin.
public class Usuario { private Goblin goblin; public Usuario(Goblin goblin) { this.goblin = goblin; } public void enviarInput(String input){ if(input.equals("p")) goblin.fire("parar"); if(input.equals("c")) goblin.fire("correr"); } }
A classe Game tem como função alterar o estado do goblin de acordo com acontecimentos do jogo.
public class Game { private Goblin goblin; public Game(Goblin goblin) { this.goblin = goblin; } public void tomarTiro(){ goblin.fire("cair"); } public void verNegao(){ goblin.fire("negao"); } public void sairDaRealidade(){ goblin.fire("fritar"); } public void receberBonus(){ goblin.fire("bolar"); } }
A classe de teste testgame aplica os métodos de game e usuario com o objetivo de testar as alterações de estado do goblin. Segue o código da classe de testes e os seus resultados.
public class Game { private Goblin goblin; public Game(Goblin goblin) { this.goblin = goblin; } public void tomarTiro(){ goblin.fire("cair"); } public void verNegao(){ goblin.fire("negao"); } public void sairDaRealidade(){ goblin.fire("fritar"); } public void receberBonus(){ goblin.fire("bolar"); } }
Conclusão
Este laboratório foi o de maior aprendizado pra mim, o tempo para a realização foi adequado para um laboratório duplo. A forma que o padrão Command e as classes anônimas foram explicadas e usadas possibilitaram o aprendizado de forma natural. Durante a criação da aplicação do personagem de uma game pude aplicar o que foi aprendido em relação ao padrão Observer, imagino que seria desgastante fazer o personagem receber informações automáticas da máquina de estados sem este padrão de projeto.
A explicação das exceções (nas instruções do laboratório) foi muito aquém do necessário para seu uso na construção da máquina de estados. Considerei interessante o uso das condições de guarda, seu uso possibilida uma flexibilidade maior para o usuário da maquina.
O encapsulamento da máquina de estaodos realmente criou uma boa separação entre o usuário e o responsável pela manutenção, permitindo suas alterações sem que a essência da máquina seja alterada. Considero que a máquina de estados criada cumpriu com o esperado, porém se mostra limita no sentido que não existe a opção de excluir os eventos nem transições.