Model-View-Controller (MVC)

por Rodrigo Rebouças de Almeida

Objetivo


Figura 1: Aplicação sendo acessada por várias interfaces

Sobre a MVC

Problema: 

Exemplo:

Solução sem MVC:

import java.awt.Button;
import java.awt.GridLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;

import java.awt.Frame;

public class TelaVotacao extends Frame implements ActionListener {

    private TelaResultado telaResult;
    private TelaResultadoPercentual telaResultPerc;

    private Map <String,Integer>opcoes;
    private ArrayList <String>listaOpcoes; // para obter as opções em ordem

    public TelaVotacao() {
        super("Tela de Votação - Enquete");

        telaResult = new TelaResultado(this);
        telaResult.setLocation(120, 5); // posicao na tela

        telaResultPerc = new TelaResultadoPercentual(this);
        telaResultPerc.setLocation(250, 5); // posicao na tela

        listaOpcoes = new ArrayList<String>();
        this.opcoes = new HashMap<String, Integer>();

        this.adicionaOpcao("Opção 1");
        this.adicionaOpcao("Opção 2");
        this.adicionaOpcao("Opção 3");
        this.adicionaOpcao("Opção 4");
        criarBotoes();

        telaResult.inicializar(listaOpcoes);
        telaResultPerc.inicializar(listaOpcoes);

        this.setSize(100, 120);
        this.setLayout(new GridLayout(0, 1));
        
        this.addWindowListener(new WindowAdapter() {
            public void windowClosing(WindowEvent e) {
                e.getWindow().hide();
                System.exit(0);
            }
        });
        this.show();
        telaResult.show();
        telaResultPerc.show();
    }

    /**
     * Adiciona uma opcao à enquete
     **/
    private void adicionaOpcao(String opcao) {
        listaOpcoes.add(opcao);
        this.opcoes.put(opcao, new Integer(0));
    }

    /**
     * Cria os botoes da tela de votos
     **/
    public void criarBotoes() {
        Button botao;
        for (String opcao : listaOpcoes) {
            botao = new Button(opcao);
            botao.setActionCommand(opcao);
            botao.addActionListener(this);
            this.add(botao);
        }
    }

    /**
     * Método executado ao clicar nos botões
     */
    public void actionPerformed(ActionEvent event) {
        String opcao = event.getActionCommand();
        this.votar(opcao); // incrementando o voto

        // Atualizando a tela de resultados absolutos
        telaResult.novoVoto(opcao, getVotos(opcao));

        // Atualizando a tela de resultados percentuais
        telaResultPerc.novoVoto(opcao, getVotos(opcao), getTotalVotos());
    }

    /**
     * Incrementa o voto da opção entrada
     */
    public void votar(String opcao) {
        int votoAtual = ((Integer) this.opcoes.get(opcao)).intValue();
        this.opcoes.put(opcao, new Integer(++votoAtual));
    }

    /**
     * Retorna a soma dos votos de todas as opções da enquete
     * @return int soma dos votos de todas as opções da enquete
     */
    public int getTotalVotos() {
        int total = 0;
        for (Integer votos : opcoes.values()) {
            total += votos.intValue();
        }
        return total;
    }

    /**
     * Retorna a quantidade de votos de uma opção individual
     */
    public int getVotos(String opcao) {
        return ((Integer) this.opcoes.get(opcao)).intValue();
    }
}
import java.awt.GridLayout;
import java.awt.Label;
import java.awt.Window;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;

import java.awt.Frame;

public class TelaResultado extends Window{
    
    private Map <String, Label>labels = new HashMap();
    
    public TelaResultado(Frame parent){
        super(parent);
        this.setSize(110,120);
        this.setLayout(new GridLayout(0,2)); // Grid com qualquer numero
                                              // de linhas e duas colunas
        this.add(new Label("Votos"));
        this.add(new Label());
    }

    /**
     * Recebe a lista de opcoes inicial
     */
    public void inicializar(ArrayList <String> opcoes) {
        
        Label label;
        Label votos;
        for(String opcao : opcoes){
            if(!labels.containsKey(opcao)){
                label = new Label(opcao+" - ");
                votos = new Label(""+0);
                labels.put(opcao,votos);
                this.add(label);
                this.add(votos);
            }
        }
    }

    /**
     * Atualiza a quantidade de votos para uma determinada opcao
     */
    public void novoVoto(String opcao, int nvotos) {
        Label votos;
        votos = labels.get(opcao);
        votos.setText(""+nvotos);
    }
}

A solução acima funciona, mas temos muitos problemas!

Solução com a MVC:


Figura 2: Fluxo de eventos e informações em uma arquitetura MVC

MVC - Como implementar (5 passos)

PASSO 1:

import java.util.HashMap;
import java.util.Map;
import java.util.Set;

public class EnqueteSimples {
    
    private Map <String,Integer>opcoes;
    
    public EnqueteSimples(){
        opcoes = new HashMap<String, Integer>();
    }
    
    /**
     * Adiciona uma opção para ser votada na enquete
     * @param opcao nome da opção
     */
    public void addOpcao(String opcao){
        opcoes.put(opcao,new Integer(0));
    }
    
    /**
     * Retorna um iterador de opções disponíveis na enquete
     * @return Iterator opções disponiveis na enquete
     */
    public Set <String> getOpcoes(){
        return opcoes.keySet();
    }
    
    /**
     * Incrementa um voto para opção
     * @param opcao opção que receberá voto
     */
    public void votar(String opcao){
        int votoAtual = ((Integer)opcoes.get(opcao)).intValue();
        opcoes.put(opcao,new Integer(++votoAtual));
    }
    
    /**
     * Retorna a soma dos votos de todas as opções da enquete
     * @return int soma dos votos de todas as opções da enquete
     */
    public int getTotalVotos(){
        
        int total = 0;
        for(Integer votos : opcoes.values()){
            total+= votos.intValue();
        }
        return total;
    }
    
    /**
     * Retorna a quantidade de votos de uma opção individual
     * @param opcao opção que se quer o voto
     * @return int quantidade de votos da opção
     */
    public int getVotos(String opcao){
        return (opcoes.get(opcao)).intValue();
    }
}

PASSO 2:


Figura 3: Modelo implementando padrão Observer

import java.util.List;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.Map;
import java.util.Set;

public class EnqueteSimples {
    
    private Map <String,Integer>opcoes;
    private List <EnqueteListener>enqueteListeners = new LinkedList();

    
    public EnqueteSimples(){
        opcoes = new HashMap<String, Integer>();
    }
    
    /**
     * Adiciona uma opção para ser votada na enquete
     * @param opcao nome da opção
     */
    public void addOpcao(String opcao){
        opcoes.put(opcao,new Integer(0));
        this.disparaNovaOpcao(opcao);
    }
    
    /**
     * Retorna um iterador de opções disponíveis na enquete
     * @return Iterator opções disponiveis na enquete
     */
    public Set <String> getOpcoes(){
        return opcoes.keySet();
    }
    
    /**
     * Incrementa um voto para opção
     * @param opcao opção que receberá voto
     */
    public void votar(String opcao){
        int votoAtual = (opcoes.get(opcao)).intValue();
        opcoes.put(opcao,new Integer(++votoAtual));
        this.disparaNovoVoto(opcao);
    }
    
    /**
     * Retorna a soma dos votos de todas as opções da enquete
     * @return int soma dos votos de todas as opções da enquete
     */
    public int getTotalVotos(){
        
        int total = 0;
        for(Integer votos : opcoes.values()){
            total+= votos.intValue();
        }
        return total;
    }
    
    /**
     * Retorna a quantidade de votos de uma opção individual
     * @param opcao opção que se quer o voto
     * @return int quantidade de votos da opção
     */
    public int getVotos(String opcao){
        return (opcoes.get(opcao)).intValue();
    }
    
    /**
     * Adiciona um EnqueteListener, um objeto interessado em
     * receber eventos lançados pela Enquete
     * @see EnqueteListener
     * @param listener objeto interessado em receber eventos
     */
    public synchronized void addEnqueteListener(EnqueteListener listener){
        if(enqueteListeners.contains(listener)){ return; }
        this.enqueteListeners.add(listener);
    }
    
    /**
     * Informa aos objetos interessados nos eventos lançados
     * pela Enquete que um novo voto foi contabilizado.
     */
    private synchronized void disparaNovoVoto(String opcao){
        for(EnqueteListener listeners : this.enqueteListeners){
            listeners.novoVoto(new EnqueteEvent(this,opcao));
        }
    }
    
    /**
     * Informa aos objetos interessados nos enventos lançados
     * pela Enquete que uma nova opção foi adicionada.
     */
    private synchronized void disparaNovaOpcao(String opcao){
        
        for(EnqueteListener listeners : this.enqueteListeners){
            listeners.novaOpcao(new EnqueteEvent(this,opcao));
        }
    }

}
import java.util.EventObject;

public class EnqueteEvent extends EventObject {
    
    private String opcao = null;
    private int votos = 0;
    
    public EnqueteEvent(EnqueteSimples source){
        super(source);
    }
    public EnqueteEvent(EnqueteSimples source,String opcao){
        this(source);
        this.opcao = opcao;
    }

    /**
     * Retorna a opção associada ao evento gerado.
     * A opção pode ser uma nova opção adicionada à EnqueteSimples
     * ou a opção escolhida para adicionar um novo voto.
     * @return String opção
     */
    public String getOpcao() {
        return opcao;
    }

    /**
     * Retorna o numero de votos da opcao
     * @return int votos
     */
    public int getVotos() {
        return ((EnqueteSimples)this.source).getVotos(opcao);
    }
    
    /**
     * Retorna o total de votos da enquete
     * @return int
     */
    public int getTotalVotos() {
        return ((EnqueteSimples)this.source).getTotalVotos();
    }

}
import java.util.EventListener;

public interface EnqueteListener extends EventListener {


    /**
     * Invocado quando um novo voto é contabilizado na Enquete.
     * @param event Evento gerado pela Enquete.
     */
    public void novoVoto(EnqueteEvent event);
    
    /**
     * Invocado quando uma nova opção é adicionada à Enquete.
     * @param event Evento gerado pela Enquete.
     */
    public void novaOpcao(EnqueteEvent event);
}

PASSO 3:

import java.awt.GridLayout;
import java.awt.Label;
import java.awt.Window;
import java.util.HashMap;
import java.util.Map;

import java.awt.Frame;


public class TelaResultado extends Window implements EnqueteListener{
    
    private Map <String, Label>labels = new HashMap();
    
    public TelaResultado(Frame parent){
        super(parent);
        this.setSize(110,120);
        this.setLayout(new GridLayout(0,2)); // Grid com qualquer numero
                                              // de linhas e uma coluna
        this.add(new Label("Votos"));
        this.add(new Label());

    }
    
    /**
     * @see enquete.model.EnqueteListener#novaOpcao(EnqueteEvent)
     */
    public void novaOpcao(EnqueteEvent event) {
        String opcao = event.getOpcao();

        Label label;
        Label votos;
        if(!labels.containsKey(opcao)){
            label = new Label(opcao+" - ");
            votos = new Label(""+event.getVotos());
            labels.put(opcao,votos);
            this.add(label);
            this.add(votos);
        }
    }

    /**
     * @see enquete.model.EnqueteListener#novoVoto(EnqueteEvent)
     */
    public void novoVoto(EnqueteEvent event) {
        String opcao = event.getOpcao();

        Label votos;
        votos = labels.get(opcao);
        votos.setText(""+event.getVotos());
    }

}
import java.awt.GridLayout;
import java.awt.Label;
import java.awt.Window;
import java.util.HashMap;
import java.util.Map;

import java.awt.Frame;

public class TelaResultadoPercentual extends Window implements EnqueteListener{
    
    private Map <String,Label>labels = new HashMap();


    public TelaResultadoPercentual(Frame parent){
        super(parent);
        this.setSize(180,120);
        this.setLayout(new GridLayout(0,2)); // Grid com qualquer numero
                                              // de linhas e uma coluna
        this.add(new Label("Percentual"));
        this.add(new Label());
    }
    
    /**
     * @see enquete.model.EnqueteListener#novaOpcao(EnqueteEvent)
     */
    public void novaOpcao(EnqueteEvent event) {
        String opcao = event.getOpcao();

        Label label;
        Label votos;
        if(!labels.containsKey(opcao)){
            label = new Label(opcao+" - ");
            votos = new Label(""+event.getVotos()+" %");
            labels.put(opcao,votos);
            this.add(label);
            this.add(votos);
        }
    }

    /**
     * @see enquete.model.EnqueteListener#novoVoto(EnqueteEvent)
     */
    public void novoVoto(EnqueteEvent event) {
        String opcao = event.getOpcao();

        Label votos;
        votos = labels.get(opcao);
        votos.setText(""+(event.getVotos()*100/event.getTotalVotos())+" %");
    }

}
import java.awt.Button;
import java.awt.GridLayout;
import java.awt.event.ActionListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.util.ArrayList;
import java.util.Collection;

import java.awt.Frame;

public class TelaVotacao extends Frame implements EnqueteListener{


    private Collection <String>botoes = new ArrayList();
    
        
    private ActionListener controller;

    public TelaVotacao(ActionListener controller){
        super("Tela de Votação - Enquete");
        this.setSize(100,120);
        this.setLayout(new GridLayout(0,1)); // Grid com qualquer numero
                                              // de linhas e uma coluna
        this.controller = controller;
        this.addWindowListener(new WindowAdapter() {
            public void windowClosing(WindowEvent e) {
                e.getWindow().hide();
                System.exit(0);
            }
        });
    }

    /**
     * @see enquete.model.EnqueteListener#novaOpcao(EnqueteEvent)
     */
    public void novaOpcao(EnqueteEvent event) {
        String opcao = event.getOpcao();
        Button botao;

        if(!botoes.contains(opcao)){
            botoes.add(opcao);
            botao = new Button(opcao);
            botao.setActionCommand(opcao);
            botao.addActionListener(controller);
            this.add(botao);
        }
    }

    /**
     * @see enquete.model.EnqueteListener#novoVoto(EnqueteEvent)
     */
    public void novoVoto(EnqueteEvent event) {
        // Nothing to do
    }

}

PASSO 4:

import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

public class TelaVotacaoCtrl implements ActionListener{
    
    private EnqueteSimples enquete;
    
    public TelaVotacaoCtrl(EnqueteSimples enquete){
        this.enquete = enquete;
    }

    /**
     * Evento lançado pelo clique nos botoes da TelaVotacao
     * @see java.awt.event.ActionListener#actionPerformed(ActionEvent)
     */
    public void actionPerformed(ActionEvent event) {
        enquete.votar(event.getActionCommand());
    }
}

PASSO 5:

public class Enquete{

    public static void main(String[] args) {

        // Modelo
        EnqueteSimples enquete= new EnqueteSimples();


        // Controlador da Interface "TelaVotacao"
        TelaVotacaoCtrl ctrl = new TelaVotacaoCtrl(enquete);

        // Interface que altera o estado do modelo
        TelaVotacao votacao = new TelaVotacao(ctrl);
        votacao.setLocation(5,5);
        
        // Interface que exibe o resultado absoluto da votacao
        TelaResultado resultado = new TelaResultado(votacao);
        resultado.setLocation(120,5);

        // Interface que exibe o resultado percentual da votacao
        TelaResultadoPercentual resultadoPerc = 
                                new TelaResultadoPercentual(votacao);
        resultadoPerc.setLocation(250,5);
        
        // Adicionando as interfaces interessadas na mudança do
        // estado do modelo
        enquete.addEnqueteListener(votacao);
        enquete.addEnqueteListener(resultado);
        enquete.addEnqueteListener(resultadoPerc);

        // Povoando o modelo
        enquete.addOpcao("Opção 1");
        enquete.addOpcao("Opção 2");
        enquete.addOpcao("Opção 3");
        enquete.addOpcao("Opção 4");


        // Exibindo as interfaces
        votacao.show();
        resultado.show();
        resultadoPerc.show();
    }

}

Conseqüências:


Clique aqui para fazer o download do código fonte desta aula


MVC programa