Lab3 Rodrigo Almeida

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

Introdução

O lab consistia na implementação da biblioteca StateMachine e posterior confecção de algum mini aplicativo que a utilizasse.

Desenvolvimento

StateMachine foi inteira escrita em um único arquivo, as duas classes auxiliares State e Event foram implementadas como inner classes. A razão disso é que essas classes servem simplesmente para guardar, de maneira estruturada, os eventos, estados e transições adicionados pelo usuário na máquina de estados, sendo pequenas e construídas tendo em mente que apenas a própria classe StateMachine as instanciariam.

Justamente por serem inner classes privadas cuja função principal é armazenar dados, os fields não foram encapsulados por getters e setters.

uml.png
/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
 
package StateMachine;
 
import java.util.Collection;
import java.util.Dictionary;
import java.util.Hashtable;
import java.util.LinkedList;
 
/**
 *
 * @author rsalmeidafl
 */
public class StateMachine {
 
    private class State {
        final String name;
        StateCommand onEnter;
 
        State(String name, StateCommand onEnter) {
            this.name = name;
            this.onEnter = onEnter;
        }
    }
 
    private class Event {
        final String name;
        EventCommand onEnter;
        EventGuard eventGuard;
        Hashtable<String, String> transitions = new Hashtable();
 
        Event(String name, EventCommand onEnter) {
            this.name = name;
            this.onEnter = onEnter;
        }
 
        void addTransition(String from, String to) {
            transitions.put(from, to);
        }
 
        boolean fire() throws UndefinedTransitionException {
 
            if (eventGuard != null && !eventGuard.shouldPerform())
                return false;
 
            State oldState = currentState;
 
            String destination = transitions.get(oldState.name);
            if (destination == null)
                throw new UndefinedTransitionException();
 
            State destinationState = states.get(destination);
            if (destinationState == null)
                throw new UndefinedStateException();
 
            currentState = destinationState;
 
            if (onEnter != null) {
                onEnter.execute(oldState.name);
            }
 
            if (currentState.onEnter != null)
                currentState.onEnter.execute();
 
            notifyObservers();
 
            return true;
        }
    }
 
    private Collection<Observer> observers = new LinkedList<Observer>();
    private Dictionary<String, State> states = new Hashtable<String, State>() {
        @Override
        public State get(Object key) {
            State state = super.get(key);
            if (state == null)
                throw new UndefinedStateError();
            return state;
        }
    };
 
    private Dictionary<String, Event> events = new Hashtable<String, Event>() {
        @Override
        public Event get(Object key) {
            Event event = super.get(key);
            if (event == null)
                throw new UndefinedEventError();
            return event;
        }
    };
 
    private State currentState = null;
 
    //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.put(name, new State(name, onEnter));
    }
 
    public void createState(String name) {
        createState(name, null);
    }
 
    public void setInitialState(String name) {
        this.currentState = states.get(name);
    }
 
    public void createEvent(String name, String[] from, String to, EventCommand onSuccess) {
        Event newEvent = new Event(name, onSuccess);
        events.put(name, newEvent);
        appendTransitionToEvent(name, from, to);
    }
 
    public void createEvent(String name, String[] from, String to) {
        createEvent(name, from, to, null);
    }
 
    public void createEvent(String name, String from, String to,  EventCommand onSuccess) {
        Event newEvent = new Event(name, onSuccess);
        events.put(name, newEvent);
 
        appendTransitionToEvent(name, from, to);
    }
 
    public void createEvent(String name, String from, String to) {
        createEvent(name, from, to, null);
    }
 
    public void appendTransitionToEvent(String event, String fromState, String toState) {
        events.get(event).addTransition(fromState, toState);
    }
 
    public void appendTransitionToEvent(String event, String[] fromState, String toState) {
        Event e = events.get(event);
 
        for (String s : fromState)
            e.addTransition(s, toState);
    }
 
    public void setGuardToEvent(String event,EventGuard g) {
        events.get(event).eventGuard = g;
    }
   // métodos para a execução da máquina
    public boolean fireEvent(String name) throws UndefinedTransitionException {
        return events.get(name).fire();
    }
 
    public String getCurrentStateName() {
        return currentState.name;
    }
}

Como aplicação da biblioteca, foi escolhida uma máquina de coca cola que vende um único produto, a um preço pré-determinado, aceita determinados tipos de moeda e fornece troco. Um detalhe é que essa máquina possui uma quantidade muito grande de estados caso sejam permitidas, por exemplo, moedas de 1 centavo (um estado para cada n centavos depositados), o que sugere que uma implementação melhor seria uma máquina de estados simplificada (com um estado "recebendo dinheiro"), onde um EventCommand dos eventos de depositar moedas manteria controle do dinheiro recebido e liberaria a passagem ou não (através de um EventGuard) a passagem desse estado para o "depositando coca cola").

A máquina de estados por tráz da Máquina de Coca Cola é determinística, mas a máquina de coca cola em si não, existe 10% de chances de uma moeda ficar "travada" e você perder seu investimento e ficar sem refrigerante - semelhante ao que a máquina de coca cola do H8C faz…

public class MaquinaCoca {
 
    private int targetPrice;
    private int stockSize;
    private int[] accepted_coins;
    private StateMachine status;
 
    String getStatus() {
        return status.getCurrentStateName();
    }
 
    private String getStateName(int totalPaid) {
       return String.format("%d centavos pagos", totalPaid);
    }
 
    private String getChangeStateName(int totalPaidExtra) {
         return String.format("%d de troco devido", totalPaidExtra);        
    }
 
    private String getCoinDepositEventName(int coinValue) {
        return String.format("depositar moeda de %d", coinValue);
    }
 
    StateMachine createMachine(int[] accepted_coins) {
        final StateMachine sm = new StateMachine();
 
        sm.createState("máquina emperrada");
        sm.createState("sem coca no estoque");
 
        StateCommand checkStock = new StateCommand(sm) {
 
            @Override
            public void execute() {
                check_stock();
            }
        };
 
        for (int i = 0; i < targetPrice + accepted_coins[accepted_coins.length - 1]; i += 1) {
            sm.createState(getStateName(i), (i == 0) ? checkStock : null);
        }
        for (int i = 1; i < accepted_coins[accepted_coins.length - 1]; ++i)
            sm.createState(getChangeStateName(i));
 
        EventCommand giveChangeBack = new EventCommand(sm) {
 
            @Override
            protected void execute(String previousState) {
                give_change_back();
            }
        };
 
        sm.createEvent("dispensar produto", getStateName(targetPrice), getStateName(0), giveChangeBack);
        sm.createEvent("notificar falta de estoque", getStateName(0), "sem coca no estoque");
        sm.createEvent("mais estoque colocado", "sem coca no estoque", getStateName(0));
 
        EventCommand tryDispensingProduct = new EventCommand(sm) {
            @Override
            protected void execute(String previousState) {
                try_dispensing_product();
            }
        };
 
        for (int coinValue : accepted_coins) {
            if (coinValue == accepted_coins[accepted_coins.length - 1])
                break;
            sm.createEvent(getCoinDepositEventName(coinValue), getChangeStateName(coinValue), getStateName(0));
        }
 
        for (int i = targetPrice + 1; i < targetPrice + accepted_coins[accepted_coins.length - 1]; i += 1) {
            sm.appendTransitionToEvent("dispensar produto", getStateName(i), getChangeStateName(i - targetPrice));
            for (int coinValue : accepted_coins) {
                if (i - targetPrice > coinValue) {
                    sm.appendTransitionToEvent(getCoinDepositEventName(coinValue), getChangeStateName(i - targetPrice), getChangeStateName(i - targetPrice - coinValue));
                }
            }
        }
 
        sm.createEvent("chamar técnico", "máquina emperrada", getStateName(0));
        sm.createEvent("emperrar", getStateName(0), "máquina emperrada");
 
        for (int coinValue : accepted_coins) {
            String eventName = "botar " + coinValue + " centavos";
            for (int i = 0; i < targetPrice ; i += 1) {
                if (i == 0) {
                    sm.createEvent(eventName, getStateName(0), getStateName(coinValue), tryDispensingProduct);
                    sm.setGuardToEvent(eventName, new EventGuard() {
 
                        public boolean shouldPerform() {
                            return !(sm.getCurrentStateName().equals("sem coca no estoque"));
                        }
                    }); 
                }
                else {
                    sm.appendTransitionToEvent(eventName, getStateName(i), getStateName(i + coinValue));
                    sm.appendTransitionToEvent("emperrar", getStateName(i), "máquina emperrada");
                }
            }
        }
 
        sm.setInitialState("sem coca no estoque");
 
        return sm;
    }
 
    public MaquinaCoca(int productPrice, int[] accepted_coins) {
        this.targetPrice = productPrice;
        this.accepted_coins = accepted_coins;
        this.status = createMachine(accepted_coins);
    }
 
    public void aumentarEstoque(int value) {
        this.stockSize += value;
        if (this.status.getCurrentStateName().equals("sem coca no estoque") && value >= 0) {
            try {
                this.status.fireEvent("mais estoque colocado");
            }
            catch (UndefinedTransitionException e) {}
        }
    }
 
    public void chamarTecnico() {
        try {
            this.status.fireEvent("chamar técnico");
        }
        catch (UndefinedTransitionException e) {
            System.out.println("Seu autista - chamou o técnico sem precisar!!!");
        }
    }
 
    public void deposit(int coinValue) {
        try {
            if (Math.random() > 0.9) {
                this.status.fireEvent("emperrar");
                System.out.println("A máquina acaba de emperrar - mais um otário perdeu grana tentando comprar coca");
            }
            else if (this.status.fireEvent("botar " + coinValue + " centavos")) {
                System.out.println("moeda engolida");
            }
        }
        catch (UndefinedEventError e) {
            System.out.println("A máquina não aceita moedas de " + coinValue + " centavos");
        }
        catch (UndefinedTransitionException e) {
            System.out.println("Moeda ignorada");
        }
    }
 
    private void give_change_back() {
        for (int i = accepted_coins.length - 2; i >= 0;) {
            try {
                this.status.fireEvent(getCoinDepositEventName(accepted_coins[i]));
                System.out.println("Você acaba de ganhar uma moeda de " + accepted_coins[i] + " centavos de troco!!!");
            }
            catch (UndefinedTransitionException e) {
                --i;
            }
        }
    }
 
    private void try_dispensing_product() {
        try {
            this.status.fireEvent("dispensar produto");
            System.out.println("Você acaba de ganhar uma coca!!!");
            stockSize -= 1;
        }
        catch (UndefinedTransitionException e) {
        }
    }
 
    private void check_stock() {
        if (stockSize == 0)  {
            try {
                this.status.fireEvent("notificar falta de estoque");
            } catch (UndefinedTransitionException ex) {}
        }
    }
}

A execução desse aplicativo é feita por uma interface console quick-and-dirty que dá acesso direto (via metaprogramação) aos métodos de MaquinaCoca.

public class Main {
 
    /**
     * @param args the command line arguments
     */
    public static void main(String[] args) {
        MaquinaCoca maquina = null;
        String line;
 
        Console console = System.console();
 
        int productPrice = Integer.valueOf(System.console().readLine("Qual o preco da coca hoje? "));
 
        String[] coins = System.console().readLine("Quais as moedas aceitas hoje? ").split(",");
        int[] coinValues = new int[coins.length];
 
        for (int i = 0; i < coins.length; ++i)
            coinValues[i] = Integer.valueOf(coins[i]);
 
        maquina = new MaquinaCoca(productPrice, coinValues);
 
        while ((line = console.readLine(maquina.getStatus() + "$")) != null) {
            try {
                String[] parts = line.split(" ");
                if (parts.length == 2)
                    MaquinaCoca.class.getMethod(parts[0], int.class).invoke(maquina, Integer.valueOf(parts[1]).intValue());
                else
                    MaquinaCoca.class.getMethod(parts[0]).invoke(maquina);
            } catch (IllegalAccessException ex) {}
            catch (IllegalArgumentException ex) {}
            catch (InvocationTargetException ex) {}
            catch (NoSuchMethodException ex) { System.out.println("hein?"); }
        }
    }
}

Segue abaixo o resultado da execução desse programa:

rsalmeidafl@snb-ubuntu:~/NetBeansProjects/lab3/build/classes$ java MaquinaCoca/Main
Qual o preco da coca hoje? 100
Quais as moedas aceitas hoje? 1,5,10,25,50,100
sem coca no estoque$aumentarEstoque 2
0 centavos pagos$deposit 100
Você acaba de ganhar uma coca!!!
moeda engolida
0 centavos pagos$deposit 50
moeda engolida
50 centavos pagos$deposit 25
A máquina acaba de emperrar - mais um otário perdeu grana tentando comprar coca
máquina emperrada$chamarTecnico
0 centavos pagos$deposit 50
moeda engolida
50 centavos pagos$deposit 25
moeda engolida
75 centavos pagos$deposit 50
Você acaba de ganhar uma moeda de 25 centavos de troco!!!
Você acaba de ganhar uma coca!!!
moeda engolida
sem coca no estoque$chamarTecnico
Seu autista - chamou o técnico sem precisar!!!
sem coca no estoque

$rsalmeidafl@snb-ubuntu:~/NetBeansProjects/lab3/build/classes$ java MaquinaCoca/Main
Qual o preco da coca hoje? 150
Quais as moedas aceitas hoje? 1,13,55,118
sem coca no estoque$aumentarEstoque 10000
0 centavos pagos$deposit 6
A máquina não aceita moedas de 6 centavos
0 centavos pagos$deposit 118
A máquina acaba de emperrar - mais um otário perdeu grana tentando comprar coca
máquina emperrada$chamarTecnico
0 centavos pagos$deposit 118
moeda engolida
118 centavos pagos$deposit 55
Você acaba de ganhar uma moeda de 13 centavos de troco!!!
Você acaba de ganhar uma moeda de 1 centavos de troco!!!
Você acaba de ganhar uma moeda de 1 centavos de troco!!!
Você acaba de ganhar uma moeda de 1 centavos de troco!!!
Você acaba de ganhar uma moeda de 1 centavos de troco!!!
Você acaba de ganhar uma moeda de 1 centavos de troco!!!
Você acaba de ganhar uma moeda de 1 centavos de troco!!!
Você acaba de ganhar uma moeda de 1 centavos de troco!!!
Você acaba de ganhar uma moeda de 1 centavos de troco!!!
Você acaba de ganhar uma moeda de 1 centavos de troco!!!
Você acaba de ganhar uma moeda de 1 centavos de troco!!!
Você acaba de ganhar uma coca!!!
moeda engolida
0 centavos pagos$deposit 118
moeda engolida
118 centavos pagos$deposit 13
moeda engolida
131 centavos pagos$deposit 13
moeda engolida
144 centavos pagos$deposit 118
Você acaba de ganhar uma moeda de 55 centavos de troco!!!
Você acaba de ganhar uma moeda de 55 centavos de troco!!!
Você acaba de ganhar uma moeda de 1 centavos de troco!!!
Você acaba de ganhar uma moeda de 1 centavos de troco!!!
Você acaba de ganhar uma coca!!!
moeda engolida
0 centavos pagos$

Uma crítica à biblioteca é que a criação de transições e estados não pode ser desacoplada. Isso dificultou a construção de alguns loops em createMachine, na MaquinaCoca. Mais precisamente, seria interessante se o pseudocódigo:

for (element : set) {
     if (firstElement)
         sm.createEvent(nome, de, para);
     else
         sm.appendTransitionToEvent(nome, de, para);
}

pudesse ser reescrito como:

sm.createEvent(nome);
for (element : set)
         sm.appendTransitionToEvent(nome, de, para);

Isso inclusive deixa mais claro que o método createEvent cria um evento e uma transição.

Conclusão

A maior dificuldade do laboratório foi entender exatamente o que cada método e componente da classe StateMachine, em particular achei complicado entender, a princípio, o que eram transições e o que eram estados já que o createEvent acaba criando uma transição também, e não só um estado. O resultado dos testes não foi colocado, mas o código da StateMachine passou sem necessitar de modificações nos testes fornecidos.

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