Lab3 Livia

aluno: Nome do Aluno
ano/sem: 2008/2o.
data do laboratório (num. da semana) : 06/08/2008 (2)

Introdução

Neste laboratório, desenvolvemos uma biblioteca para implementação de uma máquina de estados determinística. Para isso, usamos, principalmente, os conceitos de Encapsulamento e o padrão Observer e conseguimos desenvolver uma biblioteca simples de implementar e bastante ampla em sua aplicabilidade. Realizamos os testes pedidos nas classes internas e construímos uma State Machine (abreviada para "SM" neste relatório) da nossa preferência, a fim de utilizar as diversas funções para as quais ela foi programada e observar seu funcionamento.

Desenvolvimento: Biblioteca StateMachine

1 - Classe StateMachine

A classe StateMachine é a principal do pacote, pois todas as SM's serão subclasses desta. Foi dado o cabeçalho de todos os seus métodos públicos, para permitir testes comuns a todas as bibliotecas criadas pelos alunos. Foram acrescentados, então, variáveis privadas para armazenar estados e eventos, objetos das classes State e Event, respectivamente. Ambas são package protected, ou seja, não podem ser acessadas diretamente por classes fora do pacote. Isto permite que o usuário da biblioteca a utilize sem precisar saber detalhes de organização e funcionamento interno. Em outras palavras, aumenta o encapsulamento. Vejamos os detalhes desta classe:

1.1 - Variáveis internas e construtor:

Optei por usar HashMaps para o armazenamento dos estados e eventos da da SM, sendo a chave do tipo String, para facilitar o relacionamento entre nome e estado/evento. Os observadores são armazenados e um Vector. Os nomes dos estados iniciais e atuais (currentState e initialState) também devem ser protegidos para preservar o encapsulamento.

public class StateMachine {
 
    private Vector<Observer> vetorObservers;
    private HashMap<String,State> mapEstados;
    private HashMap<String,Event> mapEventos;
    private String initialState;
    private String currentState;
 
    public StateMachine(){
        vetorObservers = new Vector<Observer>();
        mapEstados = new HashMap <String,State>();
        mapEventos = new HashMap<String, Event>();     
        currentState=null;
    }

1.2 - Comportamento de Observável:

A classe StateMachine deve ser observável por qualquer objeto que implemente a interface Observer (será vista mais adiante). Isto significa que ela deve possuir um método para registrar observadores e outro para notificá-los quando necessário, de acordo com o padrão Observer. Estes métodos são:

public void registerObserver(Observer observer){
        vetorObservers.add(observer);
    }
    protected void notifyObservers(){
        for(Observer e: vetorObservers){
            e.update(this,this.getCurrentStateName());
        }
    }

1.3 - Métodos para a definição dos eventos e estados da SM:

A classe deve possuir métodos públicos que permitem ao usuário da biblioteca criar sua SM de interesse, definindo estados e eventos, assim como os comandos e restrições relacionadas a eles. Desta forma, existem métodos para:

a) Criar estados com ou sem comando de entrada, cujo nome é único para aquela SM, definir um destes estados existentes como sendo o inicial e verificar qual o estado atual da SM:

OBS: Note que mensagens de erro são mostradas caso se tente criar um estado com o mesmo nome de um estado existente, pois, neste caso, a classe HashMap dita que o estado antigo será substituído pelo novo com a mesma chave e isso pode gerar comportamento indesejado. Além disso, uma exceção UndefinedStateException é lançada caso um estado que não exista seja setado como inicial. Esta exceção terá, obrigatoriamente, que ser capturada quando este método for chamado.

public void createState(String name, StateCommand onEnter){
        if(mapEstados.containsKey(name)){
            JOptionPane.showMessageDialog(null, "Já existe um estado com este nome", "Erro: nome inválido",
                    JOptionPane.ERROR_MESSAGE);
            return;
        }
        mapEstados.put(name, new State(this, name, onEnter));       
    }
 
    public void createState(String name){        
        if(mapEstados.containsKey(name)){
            JOptionPane.showMessageDialog(null, "Já existe um estado com este nome", "Erro: nome inválido",
                    JOptionPane.ERROR_MESSAGE);
            return;
        }
        mapEstados.put(name, new State(this, name)); 
    }
 
    public void setInitialState(String name) throws UndefinedStateException{
        if(!mapEstados.containsKey(name)){
            throw new UndefinedStateException();
        }
        this.initialState=name;
        this.currentState=this.initialState;
    }
 
     public String getCurrentStateName(){
        return this.currentState;
    }

b) Criar eventos que partam de um ou mais estados, com ou sem comando de execução, inserir condições de guarda em um evento (i.e.: condiçoes que devem ser satisfeitas para que o evento ocorra) e relacionar uma transição a esse evento:

OBS: Aqui também aparecem mensagens de erro ao se tentar criar um evento com o mesmo nome de um evento já registrado naquela SM, pelos mesmos motivos anteriores. Ao se tentar associar eventos inexistentes a transições entre estados e/ou eventos a transições entre estados inexistentes naquela SM, são lançadas exceções do tipo UndefinedEventException e UndefinedStateException, respectivamente. Porém, ao se chamar os métodos correspondentes, eles não serão circundados por "try/catch", pois estas exceções são do tipo não-checadas. O mesmo acontece com o método para registrar condição de guarda.

public void createEvent(String name, String[] from, String to, EventCommand onSuccess){
        if(mapEventos.containsKey(name)){
            JOptionPane.showMessageDialog(null, "Já existe um evento com este nome", "Erro: nome inválido",
                    JOptionPane.ERROR_MESSAGE);
            return;
        }
        mapEventos.put(name, new Event(this, name, from, to, onSuccess)); 
    }
 
    public void createEvent(String name, String[] from, String to){
        if(mapEventos.containsKey(name)){
            JOptionPane.showMessageDialog(null, "Já existe um evento com este nome", "Erro: nome inválido",
                    JOptionPane.ERROR_MESSAGE);
            return;
        }
        mapEventos.put(name, new Event(this, name, from, to)); 
    }
 
    public void createEvent(String name, String from, String to,  EventCommand onSuccess){
        if(mapEventos.containsKey(name)){
            JOptionPane.showMessageDialog(null, "Já existe um evento com este nome", "Erro: nome inválido",
                    JOptionPane.ERROR_MESSAGE);
            return;
        }
        mapEventos.put(name, new Event(this, name, from, to, onSuccess)); 
    }
 
    public void createEvent(String name, String from, String to){
        if(mapEventos.containsKey(name)){
            JOptionPane.showMessageDialog(null, "Já existe um evento com este nome", "Erro: nome inválido",
                    JOptionPane.ERROR_MESSAGE);
            return;
        }
        mapEventos.put(name, new Event(this, name, from, to)); 
    }
 
    public void appendTransitionToEvent(String event, String fromState, String toState){
        if((!mapEstados.containsKey(fromState))||(!mapEstados.containsKey(toState)))
            throw new UndefinedStateException();        
        if(!mapEventos.containsKey(event))        
            throw new UndefinedEventException();
        mapEventos.get(event).appendTransition(fromState, toState);
    }
 
    public void appendTransitionToEvent(String event, String[] fromState, String toState){
        for(String s: fromState){
            appendTransitionToEvent(event, s, toState);
        }
    }
 
    public void setGuardToEvent(String event,EventGuard g){
    if(!mapEventos.containsKey(event))        
            throw new UndefinedEventException();
        mapEventos.get(event).setGuard(g);
    }

1.4 - Método para a execução da máquina:

O método fireEvent(String) é responsável por verificar a condição de guarda de um evento e, se for o caso, executá-lo, mudando o estado da máquina, se necessário. Caso a condição de guarda impeça sua execução, o método retorna false.

Antes disso, porém, o método verifica se este evento está registrada na SM e se ele pode ocorrer a partir do estado presente. A primeira opção gera o lançamento de uma exceção não-checada UndefinedEventException e a segundo, uma checada UndefinedTransitionException. Caso estes dois erros não ocorram e o evento não seja guardado, o método verifica se este evento leva a um estado registrado e lança uma UndefinedStateException em caso negativo.

Eis seu código:

public boolean fireEvent(String name) throws UndefinedTransitionException{
        if(!mapEventos.containsKey(name))
            throw new UndefinedEventException();
        if(!mapEventos.get(name).isFireablefromState(this.currentState)) 
            throw new UndefinedTransitionException();
 
        if(mapEventos.get(name).isGuarded()) return false;        
        String previusState=this.currentState;
        currentState=mapEventos.get(name).getStateTo(this.currentState);
        mapEventos.get(name).fireEventCommand(previusState);
        mapEstados.get(currentState).fireStateCommand();
        return true;
    }

2 - Classe State:

A classe State é package protected. Ela armazena em si a SM à qual o estado pertence, o comando de entrada (tipo StateCommand) que deve ser acionado quando se chega a este estado e o seu nome. Ela possui dois contrutores para permitir que o usuário crie estados sem comando de entrada. Neste caso, o comando onEnter é setado como null. Caso contrário, o método fireCommand é responsável por executar este comando. Desta forma, sua execução é de responsabilidade de cada objeto State, e a SM deve somente chamar o método fireCommand de cada objeto.

Eis a classe State:

class State {
 
    private StateMachine sm;
    private StateCommand onEnter;
    private String  nome;
 
    State(StateMachine sm, String nome) {
        this(sm, nome, null);
    }    
    State(StateMachine sm,String nome,StateCommand onEnter){
        this.sm=sm;
        this.nome=nome;
        this.onEnter=onEnter;        
    }    
    String getName(){
        return nome;
    }    
    void fireStateCommand(){
        if(onEnter!=null)
            onEnter.execute();
    }    
}

3 - Classe Event:

Assim como a classe State, Event é package protected. Ela conhece a SM à qual o evento pertence, assim como todas as transições entre estados e condições de guarda. Ela possui 4 construtores, atentendo às especificações impostas, que prevêem a criação de eventos com um ou vários destinos e com ou sem um EventCommand relacionado.

As condições de guarda são armazenadas em uma Vector privado, ao qual podem ser adicionados objetos do tipo EventGuard através do método público setGuard(EventGuard).

Já as transições são armazenadas em um HashMap, onde o nome do estado de origem é a chave e o nome do estado de destino é relacionado a ela, através do método appendTransition(String, String). O uso de HashMap garante que a máquina seja determinística, pois impede que haja uma transição provocada por um mesmo evento de um mesmo estado para dois ou mais estados diferentes. Note que, no método appendTransition(String, String), o registro de transições com origem no mesmo estado é barrado e uma mensagem de erro aparece, para evitar que se apague o resgistro da transição antiga no HashMap.

Uma variável também interna armazena o comando EventCommand relacionado. Caso não exista, esta variável recebe null. Ele é executado através do método fireEventCommand().

Os métodos isGuarded(), isFireableFromState(String) e getStateTo(String) são package protected e são usados para que a SM saiba se as condições de guarda estão valendo ou não, se este evento pode ocorrer a partir de um estado cujo nome é dado e a qual estado estado ele leva, respectivamente. Isto porque a SM não tem acesso direto às condições de guarda e às transições de cada evento (Encapsulamento… de novo).

Eis a classe Event:

class Event {
 
    private StateMachine sm;
    private String nome;
    private HashMap <String, String> transitions;
    private String stateTo;
    private EventCommand eventCommand;
    private Vector<EventGuard> eventGuards;
 
//construtores
    Event(StateMachine sm, String nome, String from, String to) {
        this(sm, nome, from, to, null);
    }
    Event(StateMachine sm, String nome, String from, String to, EventCommand event) {
        transitions = new HashMap <String, String>();
        eventGuards = new Vector<EventGuard>();
        this.sm=sm;
        this.nome=nome;
        appendTransition(from, to);
        this.stateTo=to;
        this.eventCommand=event;
    }
    Event(StateMachine sm, String nome, String[] from, String to) {
        this(sm, nome, from, to, null);
    }
    Event(StateMachine sm, String nome, String[] from, String to, EventCommand event) {
        transitions = new HashMap <String, String>();
        eventGuards = new Vector<EventGuard>();
        this.sm=sm;
        this.nome=nome;
        for(String s: from){
            appendTransition(s, to);
        }
        this.stateTo=to;
        this.eventCommand=event;
    }
 
//método para registrar transição de um estado para outro    
    void appendTransition(String from, String to){
        if(transitions.containsKey(from)){
            JOptionPane.showMessageDialog(null, "Um mesmo evento não pode causar uma" +
                    "transição de origem no estado '"+from+"' para dois destinos diferentes",
                    "Erro: transição inválida", JOptionPane.ERROR_MESSAGE);
            return;
        }
        transitions.put(from, to);
    }
 
//método para registrar condição de guarda
    void setGuard(EventGuard e){
        eventGuards.add(e);
    }
 
//método que retorna nome    
    String getName(){
        return nome;
    }  
 
//método para verificar se um determinado estado está registrado como origem de alguma //transição deste evento  
    boolean isFireablefromState(String stateFrom){
        if(transitions.containsKey(stateFrom))
            return true;
        return false;
    }   
 
//método para verificar se as condições de guarda estão valendo  
    boolean isGuarded(){
        boolean isGuarded=false;
        for(EventGuard e: eventGuards){
            if(!e.shouldPerform()){
                isGuarded=true;
                break;
            }
        }
        return isGuarded;
    }    
 
//método para verificar para onde o evento leva a SM, dado o estado de origem.
//caso não possa ocorrer evento naquele estado, retorna null
    String getStateTo(String stateFrom){
        return transitions.get(stateFrom);
    }
 
//dispara o comando relacionado
    void fireEventCommand(String previousState){
        if(eventCommand!=null)
            this.eventCommand.execute(previousState);
    }    
}

Observe que, assim como ocorre com State, a responsabilidade de excecução do EventCommand relacionado (caso exista) é de cada objeto Event e não da SM, que não conhece qual o comando relacionado com cada evento. Novamente… encapsulamento!

4 - Classes StateCommand, EventCommand e interface EventGuard:

Estes elementos definem comandos de estado, de evento e condições de guarda. Todos foram fornecidos nas instruções, por isso não serão mostrados aqui.
O único comentário pertinente é que, como as classes são abstratas e EventGuard é uma interface, é necessário a a criação de classes que herdem as duas primeiras ou implementem a última. Isto pode ser feito utilizando classes anônimas no momento de criação da SM. Por exemplo: o trecho a seguir foi retirado da classe Boss do programa que implementa a biblioteca (apresentado mais adiante) e mostra o uso desta ferramenta da linguagem Java.

sm.createState("irado",new lab3.stateMachine.StateCommand(this.sm) {
             public void execute() {
                 gentileza=-1;
                 generosidade=-1;
                 System.out.println("Boss: Bando de idiotas! Não valem o que ganham! Não darei uma" +
                         "segunda chance!");
             }
         });

Para que serve este recurso??
Note que, dentro do método execute() da classe-filha de StateCommand, são modificadas as variáveis "gentileza" e "generosidade", que são da classe Boss. Caso sub-classe não fosse anônima, mas sim declarada em um arquivo separado, o método execute() não "conheceria" estas variáveis.

5 - Interface Observer:

Foi dada nas instruções. Permite que toda classe que a implemente tenha um método update(StateMachine, String), que é chamado pelo objeto observado, uma SM, toda vez que ela muda de estado. O estado novo é passado como parâmetro.
Esta interface, juntamento com os métodos que tornam uma SM observável comentados acima, são responsáveis por implementar o padrão Observer.

6 - Exceções:

Todas as exceções necessárias possuem como único método o construtor, que é responsável por imprimir uma mensagem de erro na tela avisando de sua ocorrência.
As exceções UndefinedStateException e UndefinedEventException são não-checadas, ou seja, não são obrigatoriamente capturadas pelos métodos que chamam métodos que as lançam. Já a UndefinedTransactionException deve ser sempre lançada para "cima", pois é checada.

7 - Diagrama, Testes e Arquivos:

O diagrama das classes da biblioteca segue abaixo:

Diagram

Foram realizados os testes pedidos e todos foram bem sucedidos, como mostra a tela abaixo:

Resultado

O pacote que contém a biblioteca está AQUI.

Desenvolvimento: Uma Aplicação de StateMachine

Para verificar como o pacote pode ser aplicado, criei as classes Employee e Boss com comportamento de SM. Os grafos a seguir definem o comportamento de cada uma:

- SM Employee:

Employee

- SM Boss:

Boss

Um objeto Boss pode ser observado por Employees, ou seja, Employee implementa a interface BossObserver abaixo:

public interface BossObserver {
    void updateFromBoss(Boss b);
}

Ambas as SM's são subclasses do classe WithSM. Ela é similar à dada nas instruções, porém, aqui, é abstrata e possui o método:

abstract void setSM();

Este método tem que ser implementado por todas as subclasses de WithSM e deve conter as especificações da máquina, i.e., estados, eventos, comandos e condições de guarda.

O cabeçalho de Employee, seu construtor e sua implementação do método setSM() se encontram abaixo, brevemente comentados. Note o uso de classes anônimas para criação de comandos de evento, de estado e condições de guarda.
Note também que todos os estados e eventos possuem comandos que mostram alguma mensagem na tela, para que possamos checar posteriormente se tudo está funcionando corretamente.

void setSM() { 
        //estados
        sm.createState("motivado", new lab3.stateMachine.StateCommand(sm) {
            public void execute() {
                System.out.println("Employee: Estou motivado!");
            } } );
        sm.createState("desmotivado", new lab3.stateMachine.StateCommand(sm) {
            public void execute() {
                System.out.println("Employee: Estou desmotivado! Sugou!");
            } } );
        sm.createState("fazendo obrigacao", new lab3.stateMachine.StateCommand(sm) {
            public void execute() {
                System.out.println("Employee: Estou só fazendo minha obrigação...");
            } } );
 
        sm.setInitialState("motivado");
 
        //evento "dar bronca" com condição de guarda que impede qualquer reação
        //do employee caso ele estaja ganhando mais de um milhão
        String[] from = {"desmotivado", "fazendo obrigacao"};
        sm.createEvent("dar bronca", from, "desmotivado", new lab3.stateMachine.EventCommand(sm) {
            public void execute(String previousState) {
                System.out.println("Employee: Levei uma bronca! Sugou!");
            } });
        sm.appendTransitionToEvent("dar bronca","motivado","fazendo obrigacao");
        sm.setGuardToEvent("dar bronca", new lab3.stateMachine.EventGuard() {
            public boolean shouldPerform(){
               if(isGanhandoMaisUmMilhao()){
                   System.out.println("Aguento qualquer bronca por esse salário!");
                   return false;
               }
               return true;
            }});
 
        //evento "elogiar"
        sm.createEvent("elogiar", from, "fazendo obrigacao", new lab3.stateMachine.EventCommand(sm) {
            public void execute(String previousState) {
                System.out.println("Employee: Fui elogiado!");
            }});
        sm.appendTransitionToEvent("elogiar","motivado","motivado");
 
        //evento "diminuir salario", com condição de guarda que impede que o salário seja
        //menor que o salário mínimo
        String[] from2={"desmotivado", "fazendo obrigacao","motivado"};
        sm.createEvent("diminuir salario", from2, "desmotivado", new lab3.stateMachine.EventCommand(sm) {
            public void execute(String previousState) {
                System.out.println("Employee: Abaixaram meu salario!");
                salario=salario/2;
                if(isGanhandoMenosSalarioMinimo()) salario=400;
            }});
        sm.setGuardToEvent("diminuir salario", new lab3.stateMachine.EventGuard() {
            public boolean shouldPerform(){
                if(isGanhandoSalarioMinimo()){
                    System.out.println("Employee: Já estou ganhando salário minimo! Não dá para abaixar mais!");
                    return false;
                }
                return true;
            }});
 
        //evento "aumentar salario"
        String[] from3={"fazendo obrigacao","motivado"};
         sm.createEvent("aumentar salario", from3, "motivado", new lab3.stateMachine.EventCommand(sm) {
            public void execute(String previousState) {
                System.out.println("Employee: Subiram meu salario!");
                salario=salario*2;
            }});
        sm.appendTransitionToEvent("aumentar salario","desmotivado","fazendo obrigacao"); 
    }

Como Employee implementa BossObserver, deve conter o método updateFromBoss(Boss):

public void updateFromBoss(Boss b) {
        if(b.getGenerosidade()==1)
            this.fire("aumentar salario");
        else if(b.getGenerosidade()==-1) this.fire("diminuir salario");
        if(b.getGentileza()==1)
            this.fire("elogiar");
        else if(b.getGentileza()==-1) this.fire("dar bronca");
    }

Além disso, um Employee pode puxar o saco de um Boss, o que pode fazer com que este Boss mude de estado. Isto é implementado pelo método puxarSaco(Boss):

public void puxarSaco(Boss b){
        b.terSacoPuxado();
    }

Já a classe Boss deve ter comportamento de observável, ou seja, deve poder registrar e manter registro de Observers relacionados e deve conter método para notificar os observadores de qualquer evento ocorrido. Estas funcionalidades são completamente realizadas pelas variáveis e métodos apresentados na parte do código de Boss mostrada a seguir:

public class Boss extends WithSM
{
    private int generosidade;
    private int gentileza;
    private Vector<BossObserver> subordinados = new Vector<BossObserver>();
 
    public Boss() {
         super();
    }    
    public void registerObserver(BossObserver observer){
        subordinados.add(observer);
    } 
    private void notifyObservers() {
        for(BossObserver obs : subordinados) {
            obs.updateFromBoss(this);
        }
    }
    public void fire(String event){
        super.fire(event);
        this.notifyObservers();
    }

A especificação da SM representada por Boss está na método setSM(), como na classe Employee:

void setSM() {
 
    //estados
         sm.createState("satisfeito", new lab3.stateMachine.StateCommand(this.sm) {
            public void execute() {
                gentileza=1;
                generosidade=1;
                System.out.println("Boss: Estou feliz! Elogios e dinheiro para todos!");
            }});
         sm.createState("insatisfeito", new lab3.stateMachine.StateCommand(this.sm) {
             public void execute() {
                 gentileza=0;
                 generosidade=0;
                 System.out.println("Boss: Estou insatisfeito... Mas vcs têm uma segunda chance");
             }});
         sm.createState("irado",new lab3.stateMachine.StateCommand(this.sm) {
             public void execute() {
                 gentileza=-1;
                 generosidade=-1;
                 System.out.println("Boss: Bando de idiotas! Não valem o que ganham! Não darei uma" +
                         "segunda chance!");
             }});
         sm.setInitialState("satisfeito");
 
 //evento "trabalhar mal"
         String from[] = {"insatisfeito", "irado" };
         sm.createEvent("trabalhar mal", from, "irado", new lab3.stateMachine.EventCommand(sm) {
            public void execute(String previousState) {
                System.out.println("Boss: Estou vendo trabalho ruim... Desmotivei!");
            }});
         sm.appendTransitionToEvent("trabalhar mal", "satisfeito", "insatisfeito");
 
 //evento "trabalhar bem"
         String from2[]  = {"insatisfeito", "satisfeito" };
         sm.createEvent("trabalhar bem", from2, "satisfeito",new lab3.stateMachine.EventCommand(sm) {
            public void execute(String previousState) {
                System.out.println("Boss: Estou vendo bom trabalho... Motivei!");
            }});
         sm.appendTransitionToEvent("trabalhar bem", "irado", "irado");
 
 //evento "puxar saco"
         sm.createEvent("puxar saco", "irado", "insatisfeito",new lab3.stateMachine.EventCommand(sm) {
            public void execute(String previousState) {
                System.out.println("Boss: Hm.. Estao puxando meu saco!");
            }});
         sm.appendTransitionToEvent("puxar saco", from2, "satisfeito");
     }

O projeto completo com os códigos de Boss, Employee, BossObserver e WithSM estão AQUI. Eles fazem parte do projeto cuja estrutura se encontra abaixo:

DiagramaAplicacao

Foram realizados alguns testes, instanciando objetos Boss e Employees e testando seus métodos no próprio BlueJ. Os resultados podem ser vistos na tela de comando do programa, onde aparecem as mensagens correspondentes a todos os comandos de evento, estado e às condições de guarda (quando impedem uma execução):

TerminalMsgs

Mais testes podem ser feitos abrindo o projeto no BlueJ.

Conclusão

Neste laboratório, construimos uma aplicação um pouco maior. Assim, pudermos ver a importância do uso de padrões na programação OO.
A idéia de construirmos uma biblioteca para, depois, usá-la foi muito boa, pois pudermos perceber que uma biblioteca construída aplicando-se o conceito do encapsulamento é muito mais simples de ser utilizada posteriormente por outra aplicação.

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