Lab3

Objetivo e Motivação

O objetivo deste laboratório é desenvolver uma biblioteca de máquina de estados, a partir de uma descrição funcional, casos de teste e especificações de interface, e aplicar esta biblioteca em alguns exemplos simples. Além disto, este laboratório possibilitará ao aluno:

  • Exercitar modelagem OO para o desenvolvimento de uma aplicação com fins práticos.
  • Exercitar o uso do padrão Observer
  • Conhecer e exercitar o padrão Command
  • Conhecer e exercitar o uso de classes anônimas em Java

Desenvolvimento

Máquina de Estados Finita movida a eventos

Vamos desenvolver uma máquina de estados finita, determinísta, movida a eventos. Esta máquina possui vários estados, cada um com um nome (único para a máquina). Em um determinado momento, a máquina se encontra em um e somente um destes estados. No início do processamento, a máquina se encontra no estado inicial. Uma máquina também possui transições, que permitem a ela mudar de um estado para o outro. As transições são direcionadas. A máquina muda de estado quando recebe um evento, que também possui um nome (também único para a máquina). Por ser determinista, de cada estado de nossa máquina só pode sair no máximo uma transição que responde a um determinado evento.

O evento pode ser interpretado como uma entrada da máquina (cada evento seria correspondente a um elemento do alfabeto de entrada da máquina).

Além disto, nossa máquina pode ter condições de guarda, que impedem que um determinado evento seja disparado, caso o resultado da sua execução retorne um booleano falso. Pode também ter ações de entrada em estado, executadas quando a máquina chega a determinado estado. E também ter ações de evento, executadas quando um determinado evento é disparado, após passar pela condição de guarda. As ações de evento recebem como parâmetro o nome do estado no qual a máquina se encontrava anteriormente.

sm.png

Exceções

Nossa máquina deve gerar uma exceção checada (subclasse do tipo Exception) do tipo UndefinedTransitionException, que é lançada quando tenta-se executar um evento quando estivermos em um estado que não possui transição de saída para aquele evento.

Deve também gerar uma exceção não checada (subclasse de RuntimeException) do tipo UndefinedStateException, lançada quanto tenta-se transitar para um estado que não está definido na máquina. De forma semelhante, deve-se lançar uma exceção checada do tipo UndefinedEventException quando tentamos disparar um evento não definido na máquina.

classe StateMachine

Como ponto de entrada da nossa biblioteca, devemos definir uma classe StateMachine, onde ficará definida a interface pública da nossa máquina de estados. A interface desta classe já está definida, e se encontra no trecho de código abaixo. Deverá ser seguida para que os testes criados possam ser executados sem alteração.

//comportamento de observável
    public void registerObserver(Observer observer);
    protected void notifyObservers();
    //métodos para a definição da máquina
    public void createState(String name, StateCommand onEnter);
    public void createState(String name);
    public void setInitialState(String name);
    public void createEvent(String name, String[] from, String to, EventCommand onSuccess);     
    public void createEvent(String name, String[] from, String to);
    public void createEvent(String name, String from, String to,  EventCommand onSuccess);
    public void createEvent(String name, String from, String to);
    public void appendTransitionToEvent(String event, String fromState, String toState);
    public void appendTransitionToEvent(String event, String[] fromState, String toState);
    public void setGuardToEvent(String event,EventGuard g);
   // métodos para a execução da máquina
    public boolean fireEvent(String name) throws UndefinedTransitionException;
    public String getCurrentStateName();

Ações e condições de guarda

As clausulas de guarda deverão ser classes que implementam a interface EventGuard, mostrada a seguir. O método shouldPerform deverá retornar true para que o evento seja disparado normalmente.

public interface EventGuard
{
    boolean shouldPerform();
}

As ações de evento deverão ser subclasses concretas de EventCommand. O método execute deve ser chamado (quando a ação for configurada) após a transição bem sucedida da máquina, passando-se o estado anterior da mesma.

public abstract class EventCommand
{
    protected StateMachine machine;
    public EventCommand(StateMachine machine)
    {
        this.machine = machine;
    }
    public abstract void execute(String previousState);
}

As ações de chegada no estado deverão ser subclasses concretas de StateCommand. O método execute deve ser chamado (quando a ação for configurada) após a máquina entrar em determinado estado.

public abstract class StateCommand
{
    protected StateMachine machine;
    public StateCommand(StateMachine machine)
    {
        this.machine = machine;
    }
    public abstract void execute();
}

As ações e condições de guarda serão definidas, em geral, com o uso de classes anônimas, no momento da chamada dos métodos que criam os eventos e estados, e do método setGuardToEvent().

sm.createState("irado", new stateMachine.StateCommand(sm) {
            public void execute() {
                System.out.println("nunca mais serei o mesmo!");
            }
        } );

Um artigo sobre classes anônimas, caso desejem se aprofundar [http://www.developer.com/java/other/article.php/3300881]. (o que foi explicado na aula de laboratório é suficiente, no entanto).

Encapsulamento

Todo código da máquina de estados deverá ser armazenado num pacote. Somente as classes StateMachine, as classes abstratas das ações EventCommand e StateCommand, a interface EventGuard da condição de guarda deverão ser públicas. Isto irá esconder a complexidade da nossa biblioteca dos usuários e ira evitar que ações internas sejam chamadas por eles, aumentando o encapsulamento e facilitando a execução de refatorações internas que não alterem a sua interface pública.

Observer

A classe StateMachine deverá ser observável, por objetos que implementarem a interface Observer, da sua biblioteca. O notifyUpdates() de StateMachine deverá passar a máquina de estados e o estado atual ao observador no momento em que houver uma mudança de estado.

public interface Observer
{
    void update(StateMachine sm, String newStateName);
}

Aplicação da biblioteca de StateMachine - Exemplo dado

Após o desenvolvimento da biblioteca, deveremos testar seu funcionamento rodando a aplicação de exemplo mostrada em aula.

Professor:
estados-professor.png

Turma:
estados-turma.png

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: trasição não definida: " + e.getMessage());
        }
    }
 
    public String getState(){
        return sm.getCurrentStateName();
    }
}
public class Professor extends WithSM implements stateMachine.Observer, TurmaObserver
{
 
    public Professor() {
        super();
        setUpSM();
        sm.registerObserver(this);
    }
 
    public String fazerProva() {
        if(getState() == "motivado")
            return "Prova com noção";
        else if(getState() == "desmotivado" || getState() == "vacinado")
            return "Prova ruim, correção carteada";
        else if(getState() == "irado")
            return "Prova sem noção, média D";
        else
            return "Prova que nunca será feita";
    }
 
    public String prepararAula() {
        if(getState() == "motivado")
            return "Aula preparada com carinho (5 horas)";
        else if(getState() == "desmotivado" || getState() == "vacinado")
            return "Aula preparada com pressa (1 hora)";
        else if(getState() == "irado")
            return "Aula jogada (5 min)";
        else
            return "Aula que nunca será feita";
    }
 
    private boolean ganhandoMilhoes = false;
    public boolean isGanhandoMilhoesDeDolares(){
        return ganhandoMilhoes;
    }
    public void setGanhandoMilhoesDeDolares(boolean is){
        ganhandoMilhoes = is;
    }
 
    public void update(stateMachine.StateMachine sm, String newStateName) {
        System.out.println("Meu estado agora é " + newStateName);
    }
 
    public void updateFromTurma(Turma t) {
        if (t.getState() == "prestando atenção" || t.getState() == "cancerizando nos labs")
            this.fire("motivar");
        else
            this.fire("desmotivar");
    }
 
    private void setUpSM() {
 
        //criando estados
        sm.createState("motivado");
        sm.createState("desmotivado");
        sm.createState("vacinado");
        sm.createState("irado", new stateMachine.StateCommand(sm) {
            public void execute() {
                System.out.println("nunca mais serei o mesmo!");
            }
        } );
        sm.setInitialState("motivado");
 
        //evento desmotivar
        sm.createEvent("desmotivar", "motivado", "desmotivado", new stateMachine.EventCommand(sm) {
            public void execute(String previousState) {
                System.out.println("Fiquei mais desmotivado!");
            }
        });
        String[] from = {"desmotivado", "irado", "vacinado"};
        sm.appendTransitionToEvent("desmotivar", from, "irado");
        sm.setGuardToEvent("desmotivar", new stateMachine.EventGuard() {
            public boolean shouldPerform(){
               return !isGanhandoMilhoesDeDolares();  //nunca fica desmotivado se ganha milhoes
            }
        });
 
        //evento motivar
        sm.createEvent("motivar", "desmotivado", "motivado", new stateMachine.EventCommand(sm) {
            public void execute(String previousState) {
                System.out.println("Oba, fiquei mais motivado!");
            }
        });
        String[] from2 = {"irado", "vacinado"};
        sm.appendTransitionToEvent("motivar", from2, "vacinado");
        sm.appendTransitionToEvent("motivar", "motivado", "motivado");
 
    }
 
}
import stateMachine.*;
import java.util.Vector;
public class Turma extends WithSM
{
    private Vector<TurmaObserver> observers = new Vector<TurmaObserver>();
 
    public Turma() {
         super();
         sm.createState("prestando atenção");
         sm.createState("cancerizando nos labs");
         sm.createState("dormindo");
         sm.createState("usando notebook");
         sm.setInitialState("dormindo");
 
         String from[] = {"prestando atenção", "cancerizando nos labs", "dormindo", "usando notebook" };
         sm.createEvent("dormir", from, "dormindo");
         sm.createEvent("cancerizar", from, "cancerizando nos labs");
         sm.createEvent("usar_note", from, "usando notebook");
         sm.createEvent("ser_atenta", from, "prestando atenção");
    }
 
    public void registerObserver(TurmaObserver observer){
        observers.add(observer);
    }
 
    private void notifyObservers() {
        for(TurmaObserver obs : observers) {
            obs.updateFromTurma(this);
        }
    }
 
    public void fire(String event) {
        super.fire(event);
        notifyObservers();
    }
}
public interface TurmaObserver
{
    void updateFromTurma(Turma t);
}

Aplicação - Exemplo de sua escolha

Escolha um processo ou problema do seu interesse e modele-o como máquina de estados. Crie a máquina de estado para o seu exemplo e mostre a sua execução. Procure usar ao máximo as funcionalidades da biblioteca que desenvolveu.

Testes

Foi criado código de testes para a classe StateMachine, que deve ser utilizados por vocês no desenvolvimento. Como sempre, os resultados dos testes devem ser postados no relatório. Aproveitem o código de testes para solucionar qualquer dúvida que tenham em relação ao funcionamento de StateMachine.

public void testStateMachineSimple()
    {
        StateMachine sm = new StateMachine();
        sm.createState("feliz");
        sm.createState("triste");
        sm.setInitialState("triste");
        assertEquals("triste", sm.getCurrentStateName());
        sm.createEvent("jogar_bola", "triste", "feliz");
 
        try {
            sm.fireEvent("jogar_bola");
        }catch (UndefinedTransitionException e) {
            fail("não deve gerar excessão");
        }
 
        assertEquals("feliz", sm.getCurrentStateName());
 
        try {
            sm.fireEvent("jogar_bola");
            fail("deve gerar excessão");
        }catch (UndefinedTransitionException e) {
        }
    }
 
    public void testArrayInFrom()
    {
        StateMachine sm = new StateMachine();
        sm.createState("feliz");
        sm.createState("triste");
        sm.createState("deprimido");
        sm.setInitialState("feliz");
        assertEquals("feliz", sm.getCurrentStateName());
        String[] from = {"triste", "feliz"};
        sm.createEvent("perder_mulher", from, "deprimido");
        try {
            sm.fireEvent("perder_mulher");
        }catch (UndefinedTransitionException e) {
            fail("não deve gerar excessão");
        }
        assertEquals("deprimido", sm.getCurrentStateName());
        try {
            sm.fireEvent("perder_mulher");
            fail("deve gerar excessão");
        }catch (UndefinedTransitionException e) {
        }
 
        sm = new StateMachine(); //outra maquina
        sm.createState("feliz");
        sm.createState("triste");
        sm.createState("deprimido");
        sm.setInitialState("triste");
        assertEquals("triste", sm.getCurrentStateName());
        String[] from2 = {"triste", "feliz"};
        sm.createEvent("perder_mulher", from2, "deprimido");
        try {
            sm.fireEvent("perder_mulher");
        }catch (UndefinedTransitionException e) {
            fail("não deve gerar excessão");
        }
        assertEquals("deprimido", sm.getCurrentStateName());
        try {
            sm.fireEvent("perder_mulher");
            fail("deve gerar excessão");
        }catch (UndefinedTransitionException e) {
        }
    }
 
    public void testMultipleTransitions() {
        StateMachine sm = new StateMachine();
        sm.createState("super_feliz");
        sm.createState("feliz");
        sm.createState("triste");
        sm.createState("deprimido");
        sm.setInitialState("super_feliz");
 
        String[] from = {"triste", "feliz"};
        sm.createEvent("perder_mulher", from, "deprimido");
 
        sm.appendTransitionToEvent("perder_mulher", "super_feliz", "triste");
 
        try {
            sm.fireEvent("perder_mulher");
        }catch (UndefinedTransitionException e) {
            fail("não deve gerar excessão");
        }
        assertEquals("triste", sm.getCurrentStateName());
 
        try {
            sm.fireEvent("perder_mulher");
        }catch (UndefinedTransitionException e) {
            fail("não deve gerar excessão");
        }
        assertEquals("deprimido", sm.getCurrentStateName());
 
        try {
            sm.fireEvent("perder_mulher");
            fail("deve gerar excessão");
        }catch (UndefinedTransitionException e) {
        }
    }
 
    public void testStateCommand() {
        StateMachine sm = new StateMachine();
        sm.createState("super_feliz");
        sm.createState("feliz");
        sm.createState("triste");
        sm.createState("deprimido");
        sm.setInitialState("super_feliz");
 
        sm.createEvent("perder_mulher", "super_feliz", "triste", new EventCommand(sm) {
            public void execute(String previousState) {
                System.out.println("Eu estou chorando!!! Não estou mais " + previousState +
                    ", agora estou " + this.machine.getCurrentStateName());
            }
        } );
 
        try {
            sm.fireEvent("perder_mulher");
        }catch (UndefinedTransitionException e) {
        }
 
    }
 
    public void testEventCommand() {
        StateMachine sm = new StateMachine();
        boolean called = false;
        StateCommand avisar = new StateCommand(sm) {
            public void execute() {
                System.out.println("Agora eu fiquei " + this.machine.getCurrentStateName());
            }
        };
        sm.createState("super_feliz");
        sm.createState("feliz", avisar);
        sm.createState("triste", avisar);
        sm.createState("deprimido", avisar);
        sm.setInitialState("super_feliz");
        sm.createEvent("perder_mulher", "super_feliz", "triste");
        try {
            sm.fireEvent("perder_mulher");
        }catch (UndefinedTransitionException e) {
        }
    }
 
    public void testGuard() {
        temSuperPoderes = true;
        StateMachine sm = new StateMachine();
        sm.createState("super_feliz");
        sm.createState("feliz");
        sm.createState("triste");
        sm.createState("deprimido");
        sm.setInitialState("super_feliz");
        sm.createEvent("perder_mulher", "super_feliz", "triste");
        sm.setGuardToEvent("perder_mulher", new EventGuard() {
            public boolean shouldPerform() {
                return !temSuperPoderes;
            }
        });
        try {
            assertFalse(sm.fireEvent("perder_mulher"));
        }catch (UndefinedTransitionException e) {
        }
        temSuperPoderes = false;
        try {
            assertTrue(sm.fireEvent("perder_mulher"));
        }catch (UndefinedTransitionException e) {
            fail("não deve gerar excessão");
        }
    }
 
    private boolean temSuperPoderes = false;
 
    public boolean getTemSuperPoderes(){
        return temSuperPoderes;
    }
 
    public void testShouldReturnErrorIfEventDoesntExist() {
        StateMachine sm = new StateMachine();
        sm.createState("feliz");
        sm.createState("triste");
        sm.setInitialState("triste");
        assertEquals("triste", sm.getCurrentStateName());
        sm.createEvent("jogar_bola", "triste", "estado_nao_existe");
 
        try {
            sm.fireEvent("evento_nao_existente");
            fail("deve gerar excessão");
        }catch (UndefinedTransitionException e) {
        }catch (UndefinedEventError e) {
        }    
 
    }
 
    public void testShouldReturnErrorIfStateDoesntExist() {
        StateMachine sm = new StateMachine();
        sm.createState("feliz");
        sm.createState("triste");
        sm.setInitialState("triste");
        assertEquals("triste", sm.getCurrentStateName());
        sm.createEvent("jogar_bola", "triste", "estado_nao_existe");
 
        try {
            sm.fireEvent("jogar_bola");
            fail("deve gerar excessão");
        }catch(UndefinedStateError e) {
        }catch(UndefinedTransitionException e) {
        } 
    }

Relatório

Não deixe de ser analítico. Não simplesmente cole seu código ou explique como funciona. Discuta a sua abordagem na modelagem da biblioteca. Anexe um diagrama de classes (feito no Dia ou aplicação semelhante) da sua modelagem. Discuta e comente o que foi utilizado (padrões, conceitos de OO, etc), e de que forma a aplicação se beneficiou (ou não deles). Discuta o encapsulamento e de que forma isto afeta os usuários da biblioteca e responsáveis pela manutenção da mesma. Proponha mudanças e melhorias.

Data limite não melável: 1o de Outubro (Quarta da 9a semana)

OBS: por ser equivalente a dois labs (em tempo) a nota deste lab terá peso 2 na composição da nota dos labs.

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