Lab3 Luty Rodrigues

aluno: Luty Rodrigues Rbieiro
ano/sem: 2008/2o.
data do laboratório (num. da semana) : 08/10/2008 (2)

Introdução

Neste laboratório, foram exercitados vários dos conceitos vistos em aula além do fato de permitir que o projeto da máquina de estados fosse desenvolvido somente a partir de algumas especificações dadas nas intruções deste Laboratório. Com isso, será possível fazer uso dessa máquina de estados em outras aplicações.

Desenvolvimento

Começamos com o desenvolvimento da máquina de estados.
Sabemos que uma máquina de estados deve ser composta por alguns estados possíveis, e também existe um conjunto de eventos possíveis, que levam a máquina de um estado para outro. Para isto, além das classes determinadas nas instruções do lab, teremos também as classes StateMachine, State e Event.
Poderemos, ao longo de uma execução, criar os possíveis estados e transições para esta máquina de estados, escolher um estado inicial, fazer as transições determinadas e verificar como o estado da máquina está se comportando. Desta forma, nossa máquina deverá possuir métodos para:

  • Criar um novo estado;
  • Definir um novo evento, determinando o estado de origem e o estado final;
  • Disparar um determinado evento;
  • Obter o estado em que a máquina se encontra naquele momento.

De fato, métodos com essas funções estão nas instruções. Existem mais alguns outros métodos, como registerObserver e notifyObservers, que serão usados para que utilizemos o padrão Observer (a classe StateMachine será observada, por algum outro objeto que queira observá-la).
Esta, por sua vez, avisará aos seus observadores quando sofrer alguma mudança de estado. Assim, esperamos que, no método de disparo de evento, apareça o método notifyObservers.
Veremos no código abaixo, a classe StateMachine, com os métodos implementados, os vetores com os estados e eventos possíveis, e um vetor com todos os observadores daquela máquina:

public class StateMachine {
    //Variaveis
    private Vector<State> estado = new Vector<State>();
    private Vector<Event> evento = new Vector<Event>();
    private State estadoAtual;
 
    private Vector<Observer> observers = new Vector<Observer>();
 
    //comportamento de observável
    public void registerObserver(Observer observer) {
        observers.add(observer);
    }
 
    protected void notifyObservers() {
        String str;
        str = getCurrentStateName();
        for(Observer obs : observers) {
            obs.update(this, str);
        }
    }
 
    //métodos para a definição da máquina
    private State findState(String str) throws UndefinedStateException {
        for(State state : estado){
            if(state.getName().equals(str) ){
                return state;
            }
        }       
        throw new UndefinedStateException();
    }
 
    private Event findEvent(String str) throws UndefinedEventException {
        for(Event event : evento) {
            if(event.getName().equals(str) ) {
                return event;
            }
        }
        throw new UndefinedEventException();
    }
 
    public void createState(String name, StateCommand onEnter) {
        State state = new State(name);
        state.setStateCommand(onEnter);
        estado.add(state); //O estado é colocado no vetor de estados possiveis
    }
 
    public void createState(String name) {
        State state = new State(name);
        estado.add(state); //O estado é colocado no vetor de estados possiveis
    }
 
    public void setInitialState(String name){
        estadoAtual = findState(name);
        estadoAtual.executeStateCommand();
    }
 
    public void createEvent(String name, String[] from, String to, EventCommand onSuccess) {
        Event ev = new Event(name);
        int i;
        int tam = from.length;
        for(i=0; i< tam; i++) {
            ev.addTransition( findState(from[i]), findState(to) );
        }
        ev.setEventCommand(onSuccess);
        evento.add(ev); //O evento é acrescentado ao vetor de eventos possiveis
    }
 
    public void createEvent(String name, String[] from, String to) {
        Event ev = new Event(name);
        int i;
        int tam = from.length;
        for(i=0; i< tam; i++) {
            ev.addTransition( findState(from[i]), findState(to) );
        }
        evento.add(ev); //O evento é acrescentado ao vetor de eventos possiveis
    }
 
    public void createEvent(String name, String from, String to,  EventCommand onSuccess) {
        Event ev = new Event(name);
        ev.addTransition(findState(from), findState(to) ); 
        ev.setEventCommand(onSuccess);
        evento.add(ev ); //O evento é acrescentado ao vetor de eventos possiveis        
    }
 
    public void createEvent(String name, String from, String to) {
        Event ev = new Event(name);
        ev.addTransition(findState(from), findState(to) ); 
        evento.add(ev ); //O evento é acrescentado ao vetor de eventos possiveis
    }
 
    public void appendTransitionToEvent(String event, String fromState, String toState) {
        Event ev = findEvent(event);
        ev.addTransition(findState(fromState), findState(toState) );        
    }
 
    public void appendTransitionToEvent(String event, String[] fromState, String toState) {
        Event ev = findEvent(event);
        int i;
        int tam = fromState.length;
        for(i=0; i<tam; i++) {
            ev.addTransition(findState(fromState[i]), findState(toState) );
        }
    }
 
    public void setGuardToEvent(String event, EventGuard g){
        Event ev = findEvent(event);
        ev.setGuardCondition(g);
    }
 
   // métodos para a execução da máquina
    public boolean fireEvent(String name) throws UndefinedTransitionException, UndefinedEventException {
        boolean flag = true;
 
        Event event = findEvent(name);
        State newState = event.doTransition(estadoAtual);
        if(!(newState instanceof State) ) {
            flag = false;
            throw new UndefinedTransitionException();
        }
        else if(newState.equals(estadoAtual) ) flag = false;
        else {
            State previousState = estadoAtual;
            estadoAtual = newState; 
            event.commandExecution(previousState.getName() );
            notifyObservers();
            estadoAtual.executeStateCommand();
        }
        return flag;
    }
 
    public String getCurrentStateName() {
        return estadoAtual.getName();
    }
}

Na classe acima, temos um método que não havia sido previsto nos comentários iniciais, que é o método setGuardToEvent. Esse método definirá uma condição de guarda, que servirá basicamente para restringir quando a transição associada a determinado evento poderá ou não ocorrer, mesmo se dispararmos o evento. Para definir essa restrição em tempo de execução, fazemos uso do padrão Command, e das classes anônimas, como podemos verificar no trecho abaixo, que inclusive faz parte da classe de testes (sm é uma instância de StateMachine):

temSuperPoderes = true;
         ......  
        sm.createEvent("perder_mulher", "super_feliz", "triste");
        sm.setGuardToEvent("perder_mulher", new EventGuard() {
            public boolean shouldPerform() {
                return !temSuperPoderes;
            }
        });

No nosso projeto, EventGuard é uma interface, assim, não poderíamos instanciá-la diretamente, pois existe pelo menos um método a ser implementado. No código acima, é feita uma instância desta classe, mas podemos verificar que, logo em seguida, o médoto shouldPerform (que é o único método a ser definido nesta interface) é implementado, assim, obtemos uma instância de EventGuard, que, por ter todos os métodos implementados, podemos considerar como uma classe concreta.
Esta seria uma classe anônima (subclasse de EventGuard, entretanto, não possui um nome).
O resultado disto é que podemos decidir como será o método shouldPerform em tempo de execução. A mesma idéia é utilizada quando sobrecarregamos os métodos createEvent e createState, utilizando as classes abstratas EventCommand e StateCommand, como podemos ver no código da classe StateMachine.
Por fim, também temos algumas excessões que poderão ser lançadas em alguns métodos de StateMachine. Temos os seguintes métodos que lançam execessões:

  • findState(String str) - este é um método que procura o State com o nome str no Vector de State's de StateMachine. Caso não encontre, significa que o State com o nome dado não existe, e é lançada a exceção UndefinedStateException.
  • findEvent(String str) - método análogo ao de cima, que procura Event's. Quando o Event de nome str não é encontrado, é lançada a exceção UndefinedEventException.
  • fireEvent(String name) - método que dispara o evento com o nome name. Para isso, é feito uso do método findEvent, que lançará a exceção referida acima, caso o Event com o nome dado não exista. Se tal Event existe, tentamos fazer a transição. Entretanto, se a partir do atual estado da máquina de estados não for possível realizar tal transição, lançamos a execeção UndefinedTransitionException.

A seguir, temos o diagrama de classes do projeto da máquina de estados:

lab3-fig1.JPG

Na figura acima, vemos uma classe Teste, que contém os testes propostos
nas intruções do lab3, com algumas pequenas modificações. Essas
modificações foram feitas nos seguintes métodos:

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) {
        } 
    }

Em testShouldReturnErrorIfEventDoesntExist(), trocamos a linha:
sm.createEvent("jogar_bola", "triste", "estado_nao_existe");
pela linha: sm.createEvent("jogar_bola", "triste", "feliz");

Do modo como estava antes, na linha que foi trocada, era gerada uma
UndefinedStateException, que não era esperada (não é feito o tratamento).
Com a modificação, esta exceção deixa de ser lançada neste ponto.

Em testShouldReturnErrorIfStateDoesntExist(), trocamos a linha:
sm.createEvent("jogar_bola", "triste", "estado_nao_existe");
pela linha: sm.createEvent("jogar_bola", "deprimido", "feliz");

Do modo como estava, a linha trocada já gerava uma exceção, que não
passava pelo tratamento try-catch. Com a modificação, nao há mais o
lançamento desta exceção (UndefinedStateException).

Feitas as modificações apontadas acima na classe de testes, apresentamos abaixo o resultado do teste realizado:

lab3-fig2.JPG

O passo seguinte é testar a situação proposta na instrução deste lab, que envolve um professor e uma turma, de acordo com o diagrama de estados e as classes definidas. Essas classes foram implementadas no BlueJ usando como package (nomeado de stateMachine) a implementação da máquina de estados feita acima. O resultado é mostrado na figura abaixo:

lab3-fig3.JPG

Se clicamos no package stateMachine acima, vemos o que está guardado dentro dele:

lab3-fig4.JPG

Aparecem acima todas as classes implementadas. As classes Professor, Turma e WithSM farão uso dessa biblioteca sem a necessidade de conhecer os detalhes dessa imlementação, ou o quão complexo é o projeto desse package. Temos aqui um bom exemplo de encapsulamento.
Os testes a seguir mostrarão as vantagens desse encapsulamento no uso da máquina de estados para esta situação:

lab3-fig5.JPG

Na figura acima, criamos uma instância de Turma e uma instância de Professor (utilizando seus construtores e inicializando seus respectivos nomes), e vamos utilizar o método registerObserver de Turma, para registrar o Professor Prof criado como observador da turma. Feito isso, podemos verificar o estado inicial de Prof e de turma1, através de seus métodos getState (ambas as classes o possuem, pois são subclasses de WithSM, que possui este método). O retorno é uma String com o nome do atual estado:

lab3-fig6.JPG

A seguir, vamos disparar o evento "usar_note", usando o método fire de turma1. Como Prof é um observador de turma1, a mudança de estado de turma1 eventualmente provocará uma mudança de estado em Prof, como podemos ver pela figura a seguir:

lab3-fig7.JPG

Vamos testar a condição de Guarda. Para isso, vamos fazer
Prof.setGanhandoMilhoesDeDolares(true) e disparar algum evento desmotivante, como "dormir".
O resultado segue abaixo:

lab3-fig8.JPG

Vemos que o evento realmente ocorreu (pois turma1 mudou de estado), e que o estado de Prof permanece o mesmo de antes (desmotivado). Apesar de não estar mostrado acima, o terminal do BlueJ também não sofreu nenhuma alteração (o que significa que nenhum evento ocorreu para Prof). Entretanto, Prof pode ser motivado, e mudar de estado. Por exemplo, podemos fazer Prof.fire("motivar"), e ver a mudança de estado de Prof:

lab3-fig9.JPG

Vemos no terminal do BlueJ que Prof avisa do evento e de sua mudança de estado. Vamos fazer com que Prof deixe de ganhar milhões (setGanhandoMilhoesDeDoares(false), não será exibido, para deixar mais espaço para teste de outras funcionalidades). Vamos agora disparar dois eventos em turma1 seguidamente para desmotivar o Prof. Como resultado:

lab3-fig10.JPG

Prof agora está irado, e diz que nunca mais será o mesmo. Podemos tentar motivá-lo, para comprovar que ele realmente está falando a verdade. Motivando-o duas vezes, teremos como resultado:

lab3-fig11.JPG

Realmente, o melhor que Prof pode ficar é vacinado. Caso ele seja desmotivado, seu estado volta para irado:

lab3-fig12.JPG

Para encerrar essa sessão de testes, exibimos outras funcionalidades de Prof, que, para o presente estado, apresentará como saídas:

lab3-fig13.JPG

Se tentarmos disparar um evento que não está definido, será mostrada a exceção UndefinedEventException, como na figura a seguir:

lab3-fig14.JPG

Como não foi feita nenhuma implementação nas classes de exceções, a única identificação que aparece é a que é vista na foto acima. Como não foi especificado nenhuma imlpementação nessas classes, as classes de exceções foram deixadas apenas estendendo as classes Exception e RuntimeException, e a mensagem será sempre algo como visto acima, mostrando a identificação da exceção (cujo nome já é auto-explicativo) e as linhas onde tal exceção ocorreu.

Pudemos ver, ao longo desses testes, uso de condições de guarda (quando o professor está ganhando milhões), StateCommand (quando o professor fica irado). Outra característica, que ficou bem clara após a reallização desses testes, é o encapsulamento. Disparamos vários eventos, vericamos os estados de Prof e turma1 diversas vezes, e isso sem fazer menção a qualquer método, variável ou classe interna do pacote stateMachine.

Aplicação da máquina de estados numa outra situação

Foi proposto, ainda no lab3, que criássemos uma situação onde pudéssemos fazer uso da máquina de estados desenvolvida. Vamos considerar a seguinte situação:

Considere um recipiente de 30L de capacidade, e suponha que dispomos de um balde com capacidade de 10L. Suponha que dispomos de água à vontade, e, utilizando o balde de 10L, devemos encher o balde de 30L. Admita também que, a qualquer momento, podemos ou colocar água do balde menor para o maior, ou abrir uma válvula1 que fica na base do recipiente de 30L (ou seja, se abrirmos essa válvula, esvaziamos completamente este recipiente), ou abrir uma válvula2 que fica na altura de 40cm (admita que o recipiente de 30L possui 1,20m de altura, e tem formado de paralelepípiedo). Nosso objetivo é encher o recipiente grande.
Podemos definir quatro possíveis estados:

  • Estado A: Recpiente grande vazio.
  • Estado B: Recipiente grande com 10L de água.
  • Estado C: Recipiente grande com 20L de água.
  • Estado D: Recipiente grande cheio (com os 30L de água).

Em cada estado, poderemos por 10L, abrir a valvula1 ou abrir a valvula2. Note que abrir a válvula2 deixa o balde com 10L, a menos que já esteja vazio antes dessa ação.
Admita ainda, que se colocamos uma tampa no balde, não poderemos mais colocar água, apenas abrir as válvulas. Esta seria ação é colocada no método setTampa, como podemos ver no código da classe Balde30L, criada para esta situação. Aproveitamos também a classe WithSM, dada nas instruções deste LAB.

import stateMachine.*;
public class Balde30L extends WithSM implements stateMachine.Observer{
    private boolean tampado = false;
 
    public Balde30L() {
        super();
        this.setUP();
        sm.registerObserver(this);
        System.out.println("Meu estado agora é " + getState() );
    }
 
    public void update(stateMachine.StateMachine sm, String newStateName) {
        System.out.println("Meu estado agora é " + newStateName);
    }
 
    private void setUP() {
 
        sm.createState("vazio");
        sm.createState("com 10L");
        sm.createState("com 20L");
        sm.createState("cheio", new StateCommand(sm) {
            public void execute() {
                System.out.println("Enchi o balde! Finalmente!!!");
            }
        });
 
        sm.setInitialState("vazio");
 
        String[] array = {"vazio" ,"com 10L", "com 20L", "cheio"};
        sm.createEvent("valvula1", array, "vazio");
        String[] array2 = {"cheio", "com 20L", "com 10L"};
        sm.createEvent("valvula2", array2, "com 10L");
        sm.appendTransitionToEvent("valvula2", "vazio", "vazio");
        sm.createEvent("por 10L", "vazio", "com 10L");
        sm.appendTransitionToEvent("por 10L", "com 10L", "com 20L");
        sm.appendTransitionToEvent("por 10L", "com 20L", "cheio");
        sm.appendTransitionToEvent("por 10L", "cheio", "cheio");
        sm.setGuardToEvent("por 10L", new EventGuard() {
            public boolean shouldPerform() {
                if(isTampado() ) System.out.println("O balde está tampado!");
                return !(isTampado() );
            }
        } );
    }
 
    public void setTampa(boolean tampa) {
        this.tampado = tampa;
        if(!tampa) System.out.println("O balde foi destampado.");
    }
 
    public boolean isTampado() {
        return this.tampado;
    }
 
}

Esse projeto, terá apenas as duas classes referidas e o package stateMachine, que contém as classes da máquina de estados. A seguir, mostramos uma sequência de testes desta situação:

lab3-fig15.JPG

Na figura acima, criamos a instância balde de Balde30L e já vemos uma mensagem indicando qual o estado inicial de balde. Em seguida, disparamos o evento "por 10L", e observamos a mudança de estado, pela mensagem mostrada no terminal. O método getState nos dará a mesma informação, a qualquer momento que for chamado, mas não o utilizaremos nestes testes. Agora, vamos tampar o balde e tentar colocar água para ver o que acontece (teste da condição de guarda):

lab3-fig16.JPG

Vamos retirar a tampa do balde (faremos setTampa(false), mas não mostraremos nas figuras a seguir ) e por 10L duas vezes, e verificar que o balde ficará cheio. O resultado disso é:

lab3-fig17.JPG

Vemos no terminal do BlueJ uma mensagem de que o balde foi destampado, e, em seguida, após por água no recipiente grande duas vezes, vemos que o balde está cheio.
Por fim, testemos as transições definidas como "valvula1" e "valvula2", como mostrado na figura a seguir:

lab3-fig18.JPG

Obs: No terminal do BlueJ mostrado nas figuras acima, aparece como nome do projeto CircuitoSequencial. Isto porque essa era minha idéia inicial, mas no fim, acabei não fazendo nada disso, só esqueci de mudar o nome.

Conclusão

Após este trabalho, ficou bem mais evidete o poder do encapsulamento da orientação a objetos. Isso também serve como um alerta de que podemos aproveitar as várias classes de Java já implementedas (como foi feito com o package stateMachine), utilizar suas funções e tornar mais simlpes a compreensão e o desenvolvimento do projeto. Também foi possível perceber a partir de certas necessidades, definir que padrões de projetos utilizar.
Pudemos ver os padrões Observer e ver as classes anônimas em ação, permitindo-nos definir as condições de guarda, os comandos a serem executados nos estados e eventos juntamente com a definição da própria máquina de estados.
Uma mudança possível na implementação da máquina, poderia ocorrer na classe StateMachine, pois, se repararmos bem, ela está um pouco grande. Por exemplo, na sobrecarga dos métodos createEvent e createState, poderíamos criar algumas funções privadas auxiliares para evitar reescrever código, o que ocorreu nestes métodos, ou mesmo, utilizar o padrão Strategy, para podermos escolher de que modo as classes (State ou Event) seriam criadas (na verdade, seria o padrão Abstract Factory, que é um caso particular).

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