Lab3 Thiago Brandão

aluno: Thiago Brandão Damasceno
ano/sem: 2008/2o.
data do laboratório (num. da semana) : 10/10/2008 (10)

Introdução

O objetivo do presente laboratório consistiu em implementar uma biblioteca em Java que permitisse a simulação de uma máquina de estados determinística qualquer.

Desenvolvimento

1) A biblioteca em Java

A biblioteca foi idealizada pensando em termos de mapeamento entre estados e eventos. Como a máquina é determinística, existe uma relação biunívoca entre eventos e estados finais, dado um estado inicial fixo. Existe também a relação biunívoca (óbvia) entre os nomes dos estados e os estados em si (quando diz-se estados e eventos, leia-se os objetos que encapsulam seus atributos e métodos). Esquematicamente:

esquema.png

Nesse sentido, serão construídas duas novas classes (adicionalmente ao que já foi pré-estabelecido no roteiro) que irão determinar as relações biunívocas acima. A classe StateAtom irá guardar cada estado da máquina de estado e informações sobre eventos, transições e comandos de estado. A classe Event irá guardar informações sobre cada evento além das cláusulas de guarda. Detalhes de cada implementação serão discutidos logo abaixo.

2) Implementação da classe StateMachine

Cada máquina de estado irá conter um Hashtable que irá guardar como chaves os nomes dos estados e como valores os objetos associados a eles. Desse modo, a tarefa de criar estados fica facilitada por métodos do tipo add.

createEvent e appendTransitionToEvent:

Para o caso desses dois métodos, escolhí uma implementação que pode dificultar a inserção e a procura de eventos, o que torna inutilizável o uso de métodos do tipo add ou contains, mas que deixa de lado a sutil diferença entre estes dois métodos (detalhes na classe Event). Achei desnecessário, apesar de parecer cômodo, utilizar uma método que adiciona transições a eventos, mas no caso de minha implementação, é um método inútil e que só dobra as preocupações do programador em relação à exceções que cada um poderia lançar (preocupações essas que foram ignoradas). Eu proporia uma método com mesmo nome (e vários overloads) que fizesse um tipo de inserção só (e bem feita).

setGuardToEvent:

Este método adiciona a cláusula de guarda em todas as transições que utilizam determinado evento. Basicamente utiliza um enumerador para percorrer todos os eventos em todos os estados possíveis para adicionar.

fireEvent

Originalmente deveria poder jogar exceções do tipo UndefinedEventException, o que foi consertado nessa implementação. Basicamente chama o método checkEvent que checa se o evento existe em algum dos estados do Hashtable de estados. Se não existir lança exceção do tipo UndefinedEventException. Se existir, continua a execução e checa se aquele estado "contém" aquele evento. Se não contiver, então a transição não está definida e uma exceção do tipo UndefinedTransitionException é lançada. Se contiver, então a transição está definida. Por último, o método getClauseGuardValue retorna um valor booleano que verifica a cláusula de guarda e que permite ou não o "estouro" do evento.Se não permitir, retorna false e continua no mesmo estado. Se permitir, então o nome do estado corrente muda. Nesse caso, se for um estado "nulo" (discutido mais adiante) então lança uma exceção não checada do tipo UndefinedStateException. Se não for nulo, chama os métodos que permitem a execução dos eventos de comando e de estado, avisam aos observadores e retorna true.

package stateMachine;
 
import java.util.Vector;
import java.util.Hashtable;
import java.util.Enumeration;
 
public class StateMachine
{
 
    Hashtable<String,StateAtom> stateTable = new Hashtable<String,StateAtom>(100);
    private String initialState;
    private String currentState;
    private Vector<Observer> observers = new Vector<Observer>();
 
    public void registerObserver(Observer observer)
    {
        observers.add(observer);
    }
 
    protected void notifyObservers()
    {
        for(Observer observer : observers)
            observer.update(this, currentState);
    }
 
    public void createState(String name, StateCommand onEnter)
    {
        stateTable.put(name, new StateAtom(name,onEnter, this));
    }
 
    public void createState(String name)
    {
 
        stateTable.put(name, new StateAtom(name, this));
    }
 
    public void setInitialState(String name)
    {
        initialState = name;
        currentState = name;
    }
 
    public void createEvent(String name, String[] from, String to, EventCommand onSuccess) 
    {
 
        StateAtom auxObj;
 
        for(String string : from)
        {
           auxObj =(StateAtom)stateTable.get(string) ;
 
           auxObj.setEvent(name, to, onSuccess);
        }
    }
 
    public void createEvent(String name, String[] from, String to) 
    {
        StateAtom auxObj;
 
        for(String string : from)
        {
           auxObj = (StateAtom)stateTable.get(string);
 
           auxObj.setEvent(name, to);
        }
    }
 
    public void createEvent(String name, String from, String to, EventCommand onSuccess) 
    {
        StateAtom auxObj;
 
        auxObj = (StateAtom)stateTable.get(from);
 
        auxObj.setEvent(name, to, onSuccess);
 
    }
 
    public void createEvent(String name, String from, String to) 
    {
        StateAtom auxObj;
 
        auxObj = (StateAtom)stateTable.get(from);
 
        auxObj.setEvent(name, to);
    }
 
    public void appendTransitionToEvent(String event, String fromState, String toState) 
    {
 
        StateAtom auxObj = stateTable.get(fromState);
 
        if(!auxObj.destinyTable.contains(toState))
           createEvent(event, fromState, toState);
    }
 
    public void appendTransitionToEvent(String event, String[] fromState, String toState) 
    {
        StateAtom auxObj;
 
        for(String string : fromState)
        {
 
           auxObj = stateTable.get(string); 
 
           if(!auxObj.destinyTable.contains(toState))
              createEvent(event, fromState, toState);
         }
 
    }
 
    public void setGuardToEvent(String event, EventGuard guard)
    {
        Event aux;
        Enumeration enmAtom = stateTable.keys();
        Enumeration enmDestiny;
 
        while(enmAtom.hasMoreElements())
        {
 
           enmDestiny = ((StateAtom)stateTable.get(enmAtom.nextElement())).destinyTable.keys();
 
           while(enmDestiny.hasMoreElements())
           {
              aux = (Event)enmDestiny.nextElement();
 
              if(aux.getEventName().equals(event))
                 aux.setGuard(guard);
           }
 
       }
 
    }
 
    public boolean fireEvent(String name) throws UndefinedTransitionException, UndefinedEventException
    {
        StateAtom var = (StateAtom)stateTable.get(currentState);
        String previous;
 
        if(var.checkEvent(name))
        {
            throw new UndefinedEventException();
        }
        else if(var.containsEvent(name))
        {
            if(var.getEventIndex(name).getGuardClauseValue())
            {
                previous = currentState;
 
                currentState = var.getStateByEvent(name);
 
                if(currentState == null)
                {
                    throw new UndefinedStateException();
                }
 
                var.getEventIndex(name).performEventCommand(previous);
 
                notifyObservers();
 
                ((StateAtom)stateTable.get(currentState)).performStateCommand();
 
                return true;
 
            }
 
            return false;                
 
        }
        else
        {
 
            throw new UndefinedTransitionException();
        }
 
    }
 
    public String getCurrentStateName()
    {
        return currentState;
    }
 
}

3) Classes adicionais: StateAtom e Event

3.1) Classe StateAtom

A classe StateAtom contém um Hashtable com cada evento que pode ser acionado a partir daquele estado e cada estado final. Como a máquina é determinística, a ordem de quem será chave ou valor não importa, de modo que escolhí o objeto do tipo Event para ser chave. Ainda pensei em implementar um Hashtable adicional para guardar transições mas achei desnecessário. Uma possível vantagem seria utilizar diretamente o nome dos eventos e evitar workarounds para busca e inserção dos mesmos. Entretanto, mantíve a idéia original pois neste caso, a preocupção com o método appendTransitionToEvent fica eliminada, já que não existe um hash (como nos estados) que guarda os eventos mas sim, que guardam os eventos em cada estado, o que torna o uso do método inútil. A única preocupação foi com a checagem de existência de estados. Para resolver esse problema, o método setEventcria um objeto StateAtom com nome nulo (se for o caso) e que irá ser utilizado no método fireEvent de StateMachine.

package stateMachine;
 
import java.util.Hashtable;
import java.util.Enumeration;
 
class StateAtom 
{
    private String stateName;
    private StateMachine stateMachine;
    private StateCommand stateCommand = null;
    Hashtable destinyTable = new Hashtable();
 
    public StateAtom(String name, StateMachine machine)
    {
        stateName = name;
        stateMachine = machine;
    }
 
    public StateAtom(String name, StateCommand command, StateMachine machine)
    {
        stateName = name;
        stateCommand = command;
        stateMachine = machine;
    }
 
    public void setEvent(String name, String to) 
    {
       if(stateMachine.stateTable.get(to) != null)
          destinyTable.put(new Event(name), stateMachine.stateTable.get(to));
       else 
          destinyTable.put(new Event(name), new StateAtom(null, this.stateMachine));  
 
    }
 
    public void setEvent(String name, String to, EventCommand command) 
    {
        if(stateMachine.stateTable.get(to) != null)
           destinyTable.put(new Event(name, command), stateMachine.stateTable.get(to));
        else
           destinyTable.put(new Event(name, command), new StateAtom(null, this.stateMachine));
 
    }
 
    public boolean containsEvent(String name)
    {
        Enumeration e = destinyTable.keys();
        Event aux;
 
        while(e.hasMoreElements())
        {
            aux = (Event)e.nextElement();
            if(aux.getEventName().equals(name))
               return true;
        }
 
        return false;
 
    }
 
    public Event getEventIndex(String name)
    {
        Enumeration e = destinyTable.keys();
        Event aux;
 
        while(e.hasMoreElements())
        {
            aux = (Event)e.nextElement();
 
            if(aux.getEventName().equals(name))
               return aux;
        }
 
        return null;
 
    }
 
    public String getStateByEvent(String name)
    {
        Enumeration e = destinyTable.keys();
        Event aux;
 
        while(e.hasMoreElements())
        {
            aux = (Event)e.nextElement();
 
            if(aux.getEventName().equals(name))
               return ((StateAtom)destinyTable.get(aux)).getStateName();
        }
 
        return null;
 
    }
 
    public String getStateName()
    {
        return stateName;
    }
 
    public void performStateCommand()
    {
        if(stateCommand != null)
           stateCommand.execute();
    }
 
    public boolean checkEvent(String name)
    {
 
        Enumeration sm = stateMachine.stateTable.elements(); 
        Event aux;
 
        while(sm.hasMoreElements())
        {
            Enumeration e = ((StateAtom)sm.nextElement()).destinyTable.keys();    
 
            while(e.hasMoreElements())
            {
                aux = (Event)e.nextElement();
 
                if(aux.getEventName().equals(name))
                    return false;
            }
        }
 
        return true;
 
    }
 
}

3.2) Classe Event

Guarda o nome do evento, o comando de evento, a cláusula de guarda e gerencia as execuções desses últimos.

package stateMachine;
 
class Event
{
   private String eventName;
   private EventCommand eventCommand = null;
   private EventGuard eventGuard = null;
 
   public Event(String name)
   {
       eventName = name;
   }
 
   public Event(String name, EventCommand command)
   {
       eventName = name;
       eventCommand = command;
   }
 
   public String getEventName()
   {
       return eventName;
   }
 
   public void setGuard(EventGuard guard)
   {
       eventGuard = guard;
   }
 
   public boolean getGuardClauseValue()
   {
       if(eventGuard != null)
          return eventGuard.shouldPerform();
       else 
          return true;
   }
 
   public void performEventCommand(String previousState)
   {
       if(eventCommand != null)
          eventCommand.execute(previousState);
   }
 
}

Diagrama de classes final:

diagrama2.png

4) Testando o exemplo das máquinas Professor/Turma

diagrama1.png

Registrando o professor como observador da turma.

maquina1.png

Verificando o estado do professor atual e o comportamento dele neste estado.

maquina4.png
maquina2.png
maquina3.png

Aqui, definimos o estado que irá ser acionado na máquina de estados da turma. Para o caso ser_atenta, o professor fica motivado. Se for usar_note ou dormir, ele fica desmotivado e pode até ficar irado, estado no qual diz-se que "nunca mais será o mesmo".

maquina5.png

Resultado do ciclo de humor do típico professor instável:

maquina6.png

Comportamento do mesmo, caso esteja no estado desmotivado:

maquina7.png
maquina8.png

Entretanto, se acontecer o improvável de ele estar ganhando milhões de dólares, ele ignora o que a turma faz.

maquina9.png

5) Exemplo customizado: Corpo Humano/Sistema Imunológico

As seguintes máquinas de estados modelam, de maneira simplificada, o ciclo de uma doença causada por vírus. O ciclo é composto por um corpo humano que responde a certos tipos de eventos específicos ,que são o aumento e a diminuição da atividade (do vírus), a cura e a vacinação (prévia), e por um sistema imunológico que fica chaveando entre dois estados, de acordo com o evento lançado no corpo humano e o nível de proteção do corpo humano, definido com uma variável interna e que serve como cláusula de guarda tanto para o aumento ou diminuição da atividade virótica. O valor da variável é controlada por métodos públicos (fatores externos, etc) e é incrementada/decrementada de 1 cada vez que forem chamados. Quando o sistema consegue atingir determinado nível, o corpo humano pode melhorar ou piorar.

Corpo Humano

exemplo1.png

Sistema Imunológico

exemplo2.png

5.1) Implementação das classes:

A implementação das classes segue o mesmo padrão utilizado do exemplo Professor/Turma. Serão explicados somente os pontos principais da lógica envolvida.

Classe SistemaImunologico

O sistema imunológico será o único observador do corpo humano. Ele irá receber o update do mesmo, verificando se o corpo humano está morto ou não, e em qual estado o corpo humano estará. Um ponto importante é que, ele pode continuar no estado ativo (devido a um possível aumento contínuo da atividade virótica, por exemplo). Para esta máquina, quando chegar no estado ativo, ele executa uma mensagem dizendo que estará aumentando a integridade do sistema, isto é, tentando aumentar o nível de proteção do mesmo (diminuir atividade). Esse tipo de aumento poderia estar mais automatizado porém não conseguí idealizar uma maneira para tal. Infelizmente, a melhora deve ser feita manualmente.

public class SistemaImunologico extends MaquinaEstados
                               implements stateMachine.Observer, ObservadorCorpoHumano
{
 
    private int nivelOrganismo;
    private boolean morto = false;
 
     public SistemaImunologico()
     {
        super();
        setUpSM();
        sm.registerObserver(this);
     }
 
    public void updateFromCorpoHumano(CorpoHumano h)
    {
        nivelOrganismo = h.getNivel();
 
        if(!h.getMorto())
        {
           if(h.getState().equals("enfraquecido") || h.getState().equals("doente"))
           {
 
               this.fire("detectar");
           }
           else if(this.getState().equals("ativo"))
           {
               this.fire("diminuir cepa");
               h.aumentarProtecao();
           }
           else
           {
               System.out.println("Sistema em espera...");
           }
        }
           else
              morto = h.getMorto();
    }
 
    public void update(stateMachine.StateMachine sm, String newStateName)
    {
        if(!newStateName.equals("corpo saudável não-imune"))
           System.out.println("Estado atual: " + newStateName);
        else if(!morto)
           System.out.println("Jamais voltará para um corpo saudável não-imune!");
        else
            System.out.println("Organismo morreu");
 
    }
 
    private void setUpSM() 
    {
 
        sm.createState("em espera");
        sm.createState("ativo");
        sm.setInitialState("em espera");
 
        sm.createEvent("detectar", "em espera", "ativo", new stateMachine.EventCommand(sm) {
            public void execute(String previouState)
            {
                System.out.println("Aumentando integridade do organismo...");
            }
        });
 
        sm.createEvent("detectar", "ativo", "ativo", new stateMachine.EventCommand(sm) {
            public void execute(String previouState)
            {
                if(!morto)
                   System.out.println("Aumentando integridade do organismo...");
            }
        });
 
        sm.createEvent("diminuir cepa", "ativo", "em espera", new stateMachine.EventCommand(sm) {
            public void execute(String previouState)
            {
                System.out.println("Organismo fortalecido.");
            }
        });
 
    }
 
}

Classe CorpoHumano

O corpo humano encapsula alguns métodos que servirão para controlar os estados de ambas as máquinas, além dos valores das variáveis internas. Quando chega nos estados morto ou corpo saudável imune, dispara eventos de estado avisando de alguma forma que são estados finais. Pontos importantes: aumentar e diminuir de atividades são eventos que dependem do valor do nível de proteção.

import stateMachine.*;
 
public class CorpoHumano extends MaquinaEstados
{
 
    private ObservadorCorpoHumano observer;
    private int nivelProtecao;
    private boolean morto = false;
 
    public CorpoHumano()
    {
         super();
 
         nivelProtecao = 5;
 
         sm.createState("corpo saudável não-imune");
         sm.createState("enfraquecido");
         sm.createState("doente");
         sm.createState("morto",new stateMachine.StateCommand(sm) {
            public void execute() {
                morto = true;
                System.out.println("R.I.P.");
            }
        } );
         sm.createState("melhorando");
         sm.createState("corpo saudável imune", new stateMachine.StateCommand(sm) {
            public void execute() {
                System.out.println("Corpo humano está imune à doença.");
            }
        } );
         sm.setInitialState("corpo saudável não-imune");
 
         sm.createEvent("aumentar atividade", "corpo saudável não-imune", "enfraquecido");
         sm.createEvent("aumentar atividade", "enfraquecido", "doente");       
         sm.createEvent("aumentar atividade", "doente", "morto");               
         sm.createEvent("aumentar atividade", "melhorando", "enfraquecido");      
         sm.createEvent("diminuir atividade", "enfraquecido", "melhorando");       
         sm.createEvent("diminuir atividade", "doente", "melhorando");       
         sm.createEvent("curar-se", "melhorando", "corpo saudável imune");
         sm.createEvent("vacinar-se", "corpo saudável não-imune", "corpo saudável imune");
 
         sm.setGuardToEvent("aumentar atividade", new stateMachine.EventGuard() {
            public boolean shouldPerform(){
               return !nivelMinimo();  
            }
        });
 
        sm.setGuardToEvent("diminuir atividade", new stateMachine.EventGuard() {
            public boolean shouldPerform(){
               return nivelMinimo();  
            }
        });
 
    }
 
     protected boolean nivelMinimo()
     {
        if(nivelProtecao >= 5)
            return true;
        else
            return false;
     }
 
    public int getNivel()
    {
        return nivelProtecao;
    }
 
    public void diminuirProtecao()
    {
        nivelProtecao--;
    }
 
    public void aumentarProtecao()
    {
        nivelProtecao++;
    }
 
    public boolean getMorto()
    {
        return morto;
    }
 
    public void registerObserver(ObservadorCorpoHumano observer)
    {
        this.observer = observer;
    }
 
    private void notifyObservers()
    {
        observer.updateFromCorpoHumano(this);
    }
 
    public void fire(String event)
    {
        super.fire(event);
        notifyObservers();
    }
 
}
exemplo3.png

5.2) Testes

Verificando em que estado está nosso sistema imunológico. Está no estado inicial dele, que é o de espera.

exemplo4.png

O corpo humano também está em seu estado inicial (corpo saudável não-imune), verificando que está em seus níveis normais. Para permitir que um aumento da atividade virótica afete seu estado, diminuímos a proteção do corpo humano…

exemplo7.png
exemplo5.png
exemplo6.png
exemplo9.png
exemplo8.png

Assim, ele vai sucessivamente mudando de estado até ele se encontar no estado morto.

exemplo10.png
exemplo11.png

6) Resultados da unidade de teste

Os resultados dos testes encontram-se abaixo. Algumas modificações na classe de teste foram realizadas para tornar o código compatível, devido às exceções checadas que não eram possíveis de tratar no código original.

teste.png
testes_resultados.png

Download da biblioteca implementada
Download do exemplo customizado

Conclusão

A prática permitiu consolidar os conceitos básicos de orientação a objetos e o uso de padrões de projeto básicos como o Observer. No caso do encapsulamento, como foi discutido, o projeto original permite algumas ações que podem ser vistas, na minha opinião, como uma liberdade a mais desnecessária (cuja proposta de melhoria já foi comentada anteriormente). Além disso, permitiu com que o aluno usasse a criatividade para ponderar diversos fatores importantes em um projeto maior, como facilidade e escalabilidade quando da implementação.

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