Lab3 Adriano Brasileiro

aluno: Adriano Brasileiro Silva
ano/sem: 2008/2o.
data do laboratório (num. da semana) : 17/10/2008 (3)

Introdução

Nesta Prática, criamos uma biblioteca para implementar uma máquina de estados com um observer, seguindo as especificações. Tal máquina deveria armazenar diversos estados e eventos e realizar a correta transição entre eles, podendo executar uma ação sempre que um evento fosse disparado ou sempre que se atingisse um estado novo.

Desenvolvimento

Primeiro foi necessário decidir de que maneira os estados e os eventos seriam armazenados. Optei por armazenar os estados em um vetor de strings. Como os eventos devem armazenar o estado de partida, de destino, seu nome e alguma eventual condição de guarda, optei por criar uma classe nova Event que armazenasse todas essas informações.

A classe Event ficou portanto o seguinte:

package stateMachine;
 
import java.util.Vector;
 
public class Event
{
    private EventCommand execute = null;
    private EventGuard guard = null;
    private String name;
    public Vector<String> from = new Vector<String>();    //Vetor de estados de origem
    public Vector<String> to = new Vector<String>();        //Vetor de estados de destino. São public para facilitar seu acesso
 
    public Event(String name)
    {
        this.name = name;
    }
 
    public String getName()
    {
        return name;
    }
 
    public void setExecute(EventCommand e)
    {
        execute = e;
    }
 
    public void setGuard(EventGuard g)
    {
        guard = g;
    }
 
    public boolean shouldGuard()
    {
        if(guard != null)
           return guard.shouldPerform();
        else 
           return true;
    }
 
    public void execute(String fromState)
    {
        if(execute != null)
            execute.execute(fromState);
    }
 
}

Uma mesma relação de origem -> destino é armazenado sob o mesmo índice, ou seja, caso os vetores estejam vazios, e se deseje inserir uma nova transição, a origem e o destino serão armazenados nos índices 0 de seus respectivos vetores. Isso ocorre pois essas informações são adicionadas simultaneamente, conforme podemos verificar no código da classe StateMachine, onde ficam todos os métodos para o funcionamento da máquina:

package stateMachine;
 
import java.util.Vector;
 
public class StateMachine
{
    private StateCommand sc;
    private String state;
    private Vector<String> states = new Vector<String>();
    private Vector<StateCommand> commands = new Vector<StateCommand>();
    private Vector<Event> events = new Vector<Event>();
    private Vector<Observer> observers = new Vector<Observer>();
 
    private void executeOnEnter(int index)
    {
        if(commands.get(index) != null)
            commands.get(index).execute();
    }
 
    //comportamento de observável
 
    public void registerObserver(Observer observer)
    {
        observers.add(observer);
    }
 
    protected void notifyObservers()
    {
        for(Observer observer : observers){
            observer.update(this, getCurrentStateName());
        }
    }
 
    //métodos para a definição da máquina
 
    public void createState(String name, StateCommand onEnter)
    {
        states.add(name);            //adiciona um estado no vetor de estados
        commands.add(onEnter);        //e ao mesmo tempo adiciona um comando de execução no mesmo índice
    }
 
    public void createState(String name)
    {
        states.add(name);
        commands.add(null);            //caso não haja comando de execução, o vetor simplesmente avança, garantindo que o
    }                            //índice do comando permaneça o mesmo do estado.
 
    public void setInitialState(String name)
    {
        if(!states.contains(name))
            throw new UndefinedStateException();
        this.state = name;
    }
 
    public void createEvent(String name, String[] from, String to, EventCommand onSuccess)
    {
        Event event = new Event(name);
        for(String x : from)            //é criada uma transição para cada origem definida no array from
        {
            event.from.add(x);            //aos vetores from e to são adicionados simultaneamente os estados de entrada e
            event.to.add(to);            //saída, de modo que eles ficam sempre com o mesmo índice
            event.setExecute(onSuccess);
        }
        events.add(event);            //após se configurar o evento, este é adicionado ao vetor de eventos da classe.
    }
 
    public void createEvent(String name, String[] from, String to)    //idem ao anterior, mas semo evento na entrada
    {
        Event event = new Event(name);
        for(String x : from)
        {
            event.from.add(x);
            event.to.add(to);
        }
        events.add(event);
    }
 
    public void createEvent(String name, String from, String to,  EventCommand onSuccess)    //idem, para apenas uma origem
    {
        Event event = new Event(name);
        event.from.add(from);
        event.to.add(to);
        event.setExecute(onSuccess);
        events.add(event);
    }
 
    public void createEvent(String name, String from, String to)    //idem, uma origem, sem execução
    {
        Event event = new Event(name);
        event.from.add(from);
        event.to.add(to);
        events.add(event);
    }
 
    public void appendTransitionToEvent(String event, String fromState, String toState)
    {
        int index = 0;                //Este método deve encontrar no vetor de eventos o evento que possui o nome declarado
        Event x = null;                //Ele então procura no vetor tal evento e armazena seu índice
        for(Event i : events)
            if(i.getName() == event)
            {
                index = events.indexOf(i);
                x = i;
                break;
            }
        if(x == null)
            throw new UndefinedEventException();
        x.from.add(fromState);            //o método então cria um novo evento a partir do evento com o nome declarado,
        x.to.add(toState);            //adiciona a transição pedida
        events.set(index, x);            //e substitui no mesmo índice de onde foi retirado
    }
 
    public void appendTransitionToEvent(String event, String[] fromState, String toState)
    {
        int index = 0;
        Event x = null;
        for(Event i : events)
            if(i.getName() == event)
            {
                index = events.indexOf(i);
                x = i;
                break;
            }
        if(x == null)
            throw new UndefinedEventException();
        for(String y : fromState)        //idem ao anterior, mas é adicionado uma transição para cada origem declarada
        {
            x.from.add(y);
            x.to.add(toState);
        }
        events.set(index, x);
    }
 
    public void setGuardToEvent(String event,EventGuard g)
    {
        Event x = null;
        int index = -1;
        for(Event i : events)
            if(i.getName() == event)        //aqui novamente busca-se o evento que possui o nome declarado e
            {                        //defini-se a cláusula de guarda
                index = events.indexOf(i);    //(talvez fosse mais fácil se eu tivesse feito um método para fazer essa busca...)
                x = i;
                break;
            }
        x.setGuard(g);
        events.set(index, x);
    }
 
    //métodos para a execução da máquina
 
    public boolean fireEvent(String name) throws UndefinedTransitionException
    {
        Event x = null;
        int index = -1;
        for(Event i : events)
            if(i.getName() == name)        //aqui é feita a mesma busca para encontrar o evento correto
            {
                x = i;
                break;
            }
        if(x == null)
            throw new UndefinedEventException();
 
        for(String y : x.from)                //aqui procura-se a transição que possui o mesmo nome que o estado atual
            if (y == getCurrentStateName())        //e armazena-se seu índice
            {
                index = x.from.indexOf(state);
                break;
            }
        if(index == -1)
            throw new UndefinedTransitionException();
 
        if(!states.contains(x.to.get(index)))
            throw new UndefinedStateException();
 
        if(x.shouldGuard())            //caso a condição de guarda permita...
        {
            x.execute(state);
            state = x.to.get(index);        //o estado atual é alterado para o estado de destino cujo índice é index
            executeOnEnter(states.indexOf(state));
            return true;
        }
 
        return false;
    }
 
    public String getCurrentStateName()
    {
        return state;
    }
 
}

As classes EventGuard, EventCommand e StateCommand são as mesmas declaradas nas instruções e a Observer é a interface padrão, com apenas o método update abstrato.

E temos por fim as classes de exceções:

package stateMachine;
 
public class UndefinedTransitionException extends Exception
{
    public UndefinedTransitionException()
    {
        super();
    }
 
}
package stateMachine;
 
public class UndefinedStateException extends RuntimeException
{
 
    public UndefinedStateException()
    {
        super();
    }
 
}
package stateMachine;
 
public class UndefinedEventException extends RuntimeException
{
 
    public UndefinedEventException()
    {
        super();
    }
 
}

Eu não tive certeza se a classe UnidentifiedEventException deveria ser checada ou não checada, pois a especificação dizia que de forma semelhante à UnidentifiedStateException, ela deveria ser checada, mesmo aquela sendo não-checada. Deixei portanto não checada, para que não se precisasse colocar "throws" em algum dos métodos da classe StateMachine, a fim de não alterar as assinaturas pedidas.

O diagrama da biblioteca ficou assim, portanto:

Lab3-1.PNG

Resultados

Criando o projeto do professor/turma, temos o seguinte diagrama:
Lab3-2.PNG

Que, ao rodar a classe de teste, temos resultado positivo:
Lab3-3.PNG

Criando instâncias do professor e da turma e registrando a turma como observável:
Lab3-4.PNG

Disparando evento "usar_note":
Lab3-5.PNG

Disparando evento "dormir":
Lab3-6.PNG

Disparando evento "cancerizar":
Lab3-7.PNG

A máquina então funciona!

Projeto Original

Para este projeto, criei uma máquina de estados Mario, tomando como base a máquina de professor/turma que possui diversos estados e segue o seguinte diagrama:
Lab3-8.PNG

Há também uma classe Console que observa a classe Mario e age de acordo com o que acontece neste, armazenando uma pontuação e exibindo um status caso o usuário execute seu método. O diagrama de classes fica então o seguinte:
Lab3-9.PNG

A classe Mario é a seguinte:

import stateMachine.*;
import java.util.Vector;
import java.util.Random;
public class Mario extends WithSM
{
    private Vector<MarioObserver> observers = new Vector<MarioObserver>();
    private int lives = 3;
 
    public Mario() {
        super();
        sm.createState("Mario");
        sm.createState("Super Mario");
        sm.createState("Fire Mario");
        sm.createState("Death", new stateMachine.StateCommand(sm) {
            public void execute() {        //assim que a máquina atinge esse estado, o evento para voltar ao estado inicial é lançado
                lives--;                //e o número de vidas é decrementado.
                System.out.println("Você perdeu uma vida!");
                fire("Reset");
            }
        });
        sm.setInitialState("Mario");
 
        String[] from1 = {"Mario", "Super Mario"};
        String[] from2 = {"Super Mario", "Fire Mario"};
 
        sm.createEvent("Cogumelo", from1, "Super Mario", new stateMachine.EventCommand(sm) {
            public void execute(String previousState) {
                System.out.println("Você pegou um Cogumelo! 1000 pontos!");
            }
        });
        sm.appendTransitionToEvent("Cogumelo", "Fire Mario", "Fire Mario");
 
        sm.createEvent("Flor de Fogo", from2, "Fire Mario", new stateMachine.EventCommand(sm) {
            public void execute(String previousState) {
                System.out.println("Você pegou uma Flor de Fogo! 2000 pontos!");
            }
        });
        sm.appendTransitionToEvent("Flor de Fogo", "Mario", "Super Mario");
 
        sm.createEvent("Hit", from2, "Mario", new stateMachine.EventCommand(sm) {
             public void execute(String previousState) {
                 String hazard = "goomba";                //apliquei a classe Random aqui para fazer com que um
                 Random generator = new Random();        //inimigo aleatório o atacasse
                 double r = generator.nextDouble();
                 if(r < 0.3) hazard = "bob-omb";
                 else if(r > 0.7) hazard = "koopa troopa";
                 System.out.println("Um " + hazard + " o acertou!");
             }
        });
        sm.appendTransitionToEvent("Hit", "Mario", "Death");
 
        sm.createEvent("Reset", "Death", "Mario", new stateMachine.EventCommand(sm) {
             public void execute(String previousState) {
                 System.out.println("Você tem " + lives + " vidas restantes!");    //este evento só pode ser disparado pela máquina,
             }                        //já que o usuário nunca consegue parar no estado "Death" sozinho
        });                        //e quando for Game Over (não há mais vidas), a condição de guarda
        sm.setGuardToEvent("Reset", new stateMachine.EventGuard() {        //impede que o usuário dispare este evento
             public boolean shouldPerform(){
                 if (lives >= 0) return true;
                 else
                 {
                     System.out.println("Game Over!");
                     return false;
                 }
            }
        });
    }
 
    public void registerObserver(MarioObserver observer){
        observers.add(observer);
    }
 
    private void notifyObservers() {
        for(MarioObserver obs : observers) {
            obs.updateFromMario(this);
        }
    }
 
    public void fire(String event) {
        super.fire(event);
        notifyObservers();
    }
 
    public void continuar()
    {            
        lives = 3;        //criei este método para que o usuário pudesse sair do estado "Death" caso
        fire("Reset");    //não houvessem mais vidas. Ao ser executado, as vidas são reiniciadas e
    }                //o evento "Reset" é disparado, para que a máquina volte ao estado "Mario"
}

E a classe Console é a seguinte:
public class Console extends WithSM implements stateMachine.Observer, MarioObserver
{
 
    private int pontos = 0;
    private int continues = 0;
    private String estado = "Mario";
 
    public Console() {
        super();
        sm.registerObserver(this);
    }
 
    public void status()            //este método é executado pelo usuário, e exibe uma mensagem contendo o status de Mario
    {
        System.out.println("\nPontuação: " + pontos
                + "\nContinues utilizados: " + continues/2        //aqui divide-se o número de continues por dois pois a cada Reset feito
                + "\nStatus atual: " + estado + "\n");        //pelo usuário, a variável é incermentada duas vezes
    }
 
    public void update(stateMachine.StateMachine sm, String newStateName) {
        System.out.println("Meu estado agora é " + newStateName);
    }
 
    public void updateFromMario(Mario m) {
        if (m.getState() == "Mario") estado = "Mario";
        else if (m.getState() == "Super Mario")
        {
            estado = "Super Mario";        //caso a máquina atinja estado de "Super Mario"
            pontos += 1000;            //1000 pontos são contabilizados
        }
        else if (m.getState() == "Fire Mario")
        {
            estado = "Fire Mario";        //caso a máquina atinja estado de "Super Mario"
            pontos += 2000;            //1000 pontos são contabilizados
        }
        else if (m.getState() == "Death")
        {
            estado = "Game Over";        //caso a máquina permanesça em estado de "Death", então é porque
            continues++;                //ocorreu um "game over", e o número de continues utilizados aumenta
        }
 
    }
 
}

Vejamos o funcionamento desta máquina, então:

Iniciada a máquina e disparado o evento "Cogumelo":
Lab3-10.PNG

disparado o evento "Flor de Fogo":
Lab3-11.PNG

disparado o evento "Hit":
Lab3-12.PNG

disparado o evento "Hit" mais três vezes:
Lab3-13.PNG

executado o método continuar():
Lab3-14.PNG

executado o método status() do Console:
Lab3-15.PNG

Conclusão

Esta prática se mostrou bem mais difícil do que o esperado, mas não se pode negar o conhecimento adquirido pelos métodos aplicados. A máquina pedida pela especificação se mostrou um pouco limitada, no sentido de que o Observer apenas observa o estado atual da máquina, e não seus eventos disparados. Isso impediu que eu contabilizasse os pontos adquiridos de forma adequada. No mais, foi muito interessante aplicar a biblioteca criada para realizar um projeto original.

Add a New Comment
Unless otherwise stated, the content of this page is licensed under Creative Commons Attribution-ShareAlike 3.0 License