Usando o pattern Observable

Samuel Mota

Aprenda a usar este pattern destinado a enviar notificações a outros objetos.



Introdução

Este tutorial irá explicar como criar uma GUI (Graphical User Interface) que não congele quando a aplicação estiver em um processo longo e se mantenha respondendo aos eventos do usuário.


Esta não é uma das tarefas mais simples de se fazer pois envolve diretamente o uso de Threads e o desenho do sistema de eventos da plataforma Java, mas é muito facilitada pela existência de mecanismos para implementar o Pattern Observer. Como o objetivo é ser apenas um tutorial caso queira se aprofundar no assunto veja este site ou procure mais sobre o pattern.

Alguns conceitos importantes:

  • Thread: é uma linha de execução do programa. A aplicação pode ter uma ou N threads, cada uma dessas linhas é executada independentemente da outra, (quase) em paralelo. O importante é que dentro de uma thread as instruções são processadas sequencialmente, com isso existe uma fila de instruções. É um recurso para permitir a uma mesma aplicação usar de forma mais produtiva o processamento da máquina fazendo várias tarefas ao mesmo tempo.


Exemplo
Para este exemplo utilizaremos uma tela com uma barra de progresso para manter nosso feliz usuário informado sobre o processo.



O processo será simulado, mas poderá ser qualquer atividade que seu programa precise executar em resposta a um evento, não necessariamente um click de botão, neste caso o seguinte código será usado para representar o processo:

1 for(int i = 0;i < classeGUI.TAM_PROCESSO; i++) {
2     // Este é um processo muito muito longo que congelaria nossa tela, oh!
3 }


Neste exemplo a cada 10 iterações do loop vamos atualizar a barra de progresso e ao final vamos atualizar a mensagem para o usuário.

O problema
Quando executamos um processo em resposta a um evento a tela congela, ou seja, não responde a nenhum evento enquanto não for terminado o evento anterior e isso inclui redesenhar a tela para informar o usuário do progresso por exemplo.
A tela ficaria assim ao alternar de aplicação no Windows:



O código que gerou este problema é o seguinte:

01 package br.com.guj;
02 
03 import java.awt.BorderLayout;
04 import java.awt.event.ActionEvent;
05 import java.awt.event.ActionListener;
06 import javax.swing.JButton;
07 import javax.swing.JFrame;
08 import javax.swing.JLabel;
09 import javax.swing.JProgressBar;
10 import javax.swing.JTextField;
11 
12 public class classeGUI extends JFrame {
13     // Nro de iterações para o loop de simulação do processo
14     public static int TAM_PROCESSO = 100000000;
15     
16     // Variaveis da interface
17     private JProgressBar barradeprogresso;
18     private JButton botaoOK;
19     private JTextField texto;
20     private JLabel label;
21     
22     /**
23      * Construtor padrão, instancia e monta objetos da tela 
24      */
25     protected classeGUI() {
26         Container contentPane = getContentPane();
27           contentPane.setLayout(new BorderLayout());
28         
29         botaoOK = new JButton("Processar");
30         botaoOK.addActionListenernew ActionListener() {
31             public void actionPerformed(ActionEvent e) {
32                 for(int i=0;i < classeGUI.TAM_PROCESSO; i++) {
33                     // Este é um processo muito muito longo que congelaria nossa tela, oh!
34                 }
35             }            
36         });
37         
38         texto = new JTextField();    
39         
40         label = new JLabel("Programa Exemplo");
41         label.setHorizontalAlignment(JTextField.CENTER);
42         
43         barradeprogresso = new JProgressBar();
44         contentPane.add(label,BorderLayout.NORTH);
45         contentPane.add(texto,BorderLayout.CENTER);
46         contentPane.add(botaoOK,BorderLayout.WEST);
47         contentPane.add(barradeprogresso,BorderLayout.SOUTH);
48         
49         setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
50         
51         pack();
52     }
53     
54     /**
55      * Instancia e mostra a tela do usuário
56      @param args Parâmetros da linha de execução
57      */
58     public static void main(String[] args) {
59         new classeGUI().show();
60     }
61 }


O código acima é bastante simples e apenas executa o processo no tratamento do evento, vamos deixar para estudar o código correto mais adiante.

A origem desse comportamento está no modelo adotado pelo Java para gerenciar os eventos, ou seja, foi a opção escolhida ao desenhar a arquitetura da plataforma.
Existe uma única thread conhecida como event-dispatching thread onde todos os eventos são colocados e conforme vimos no início deste tutorial todas as instruções de uma thread são executadas em sequência e por isso enquanto nosso processo não terminar nenhum outro evento será processado.

A solução
A solução lógica é separarmos nosso processo da tela e criarmos uma thread só para nosso processo.
Para criar threads podemos utilizar a herança da classe Thread ou implementar a interface Runnable, a explicação sobre threads em java está além do objetivo deste tutorial mas você pode entender melhor o assunto lendo o The Java Tutorial.

Para fazermos essa separação utilizamos também uma interface e uma classe, Observable e Observer respectivamente.

A interface Observer deve ser implementada por todos os objetos que queiram ser notificados de mudanças em outros objetos. Ela define apenas um método a ser implementado: update(Observable o, Object arg) , que será chamado a cada notificação, e é nele que faremos o controle para atualizarmos a tela.

A classe Observable deve ser implementada por todos os objetos que queiram notificar mudanças a outros objetos e ela expõe vários métodos para permitir a notificação dos observadores.
No nosso caso, a tela deve ser notificada e por isso implementa a interface e o processo deverá notificar alterações e por isso herda a classe.

É importante notar que a maioria dos componentes do Swing não são thread safe, ou seja, não fazem o controle de acesso por thread, por isso caso seu programa utilize entradas do usuário na tela certifique-se de carrega-las no seu objeto do processo antes do processamento pois caso haja mudanças nos dados durante o processamento e o processo tentar utiliza-las poderá haver inconsistencia nos parâmetros.



Observable
Nosso processo será observado, por isso a cada mudança significativa vamos notificar os observadores.
Para notificar usamos um dos métodos notify disponíveis, neste caso usamos o notifyObserver(Object) e nele passamos um objeto que terá algum significado para o objeto notificado.

Quando quisermos enviar as notificações setamos nosso objeto como alterado chamando o método setChanged().

Veja o código abaixo:

01 package br.com.guj;
02 
03 import java.util.Observable;
04 import java.util.Observer;
05 
06 /**
07  @author smota
08  */
09 public class classeProcesso
10     extends Observable
11     implements Runnable {
12 
13     /**
14      * Construtor que recebe um objeto que irá observa-lo
15      @param observador Objeto que irá acompanhar as mudanças deste objeto
16      */
17     public classeProcesso(Observer observador) {
18         //Adiciona o objeto observador a lista de observadores
19         addObserver(observador);
20         //...outras inicializações
21     }
22     
23     /**
24      * Ponto de entrada da Thread.
25      @see java.lang.Runnable#run()
26      */
27     public void run() {
28         int i;
29         for(i=0; i<= classeGUI.TAM_PROCESSO; i++) {
30             //Notifica o processamento a cada 10 iterações
31             if((i % 10 == 0)) {
32                 notifyObservers(new Integer(i));
33                 setChanged();
34             }
35         }
36         
37         //Notifica fim do processo
38         notifyObservers(new Boolean(true));
39         setChanged()
40     }
41 
42 }


Simples assim? Sim... a complexidade não está no código mas sim no significado das mensagens para o objeto notificado.


Observer
Como vimos observer será nossa tela, o código abaixo faz o serviço:

001 package br.com.guj;
002 
003 import java.awt.BorderLayout;
004 import java.awt.Container;
005 import java.awt.event.ActionEvent;
006 import java.awt.event.ActionListener;
007 import java.util.Observable;
008 import java.util.Observer;
009 import javax.swing.JButton;
010 import javax.swing.JFrame;
011 import javax.swing.JLabel;
012 import javax.swing.JProgressBar;
013 import javax.swing.JTextField;
014 
015 /**
016  @author smota
017  */
018 public class classeGUI 
019     extends JFrame
020     implements Observer {
021     //Nro de iterações para o loop de simulação do processo
022     public static int TAM_PROCESSO = 1000000;
023     
024     //Variaveis da interface
025     private JProgressBar barradeprogresso;
026     private JButton botaoOK;
027     private JTextField texto;
028     private JLabel label;
029     
030     //Variavel de controle da thread do processo
031     private Thread processo;
032     
033     /**
034      * Executa o processo da aplicação
035      */
036     private void executaProcesso() {
037         if(processo==null) { //Instancia a thread SE não existir uma
038             processo = new Thread(new classeProcesso(this));
039             processo.start();
040         else {
041             System.out.println("O processo ainda está em execução");
042         }    
043     }
044     
045     /**
046      * Construtor padrão, instancia e monta objetos da tela 
047      */
048     protected classeGUI() {
049         Container contentPane = getContentPane();
050           contentPane.setLayout(new BorderLayout());
051         
052         botaoOK = new JButton("Processar");
053         
054         //Listener do botao
055         botaoOK.addActionListenernew ActionListener() {
056             public void actionPerformed(ActionEvent e) {
057                 executaProcesso();
058             }            
059         });
060         
061         texto = new JTextField();    
062         
063         label = new JLabel("Programa Exemplo");
064         label.setHorizontalAlignment(JTextField.CENTER);
065         
066         barradeprogresso = new JProgressBar();
067         barradeprogresso.setMaximum(classeGUI.TAM_PROCESSO);
068         
069         contentPane.add(label,BorderLayout.NORTH);
070         contentPane.add(texto,BorderLayout.CENTER);
071         contentPane.add(botaoOK,BorderLayout.WEST);
072         contentPane.add(barradeprogresso,BorderLayout.SOUTH);
073         
074         setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
075         
076         pack();
077     }
078     
079     /**
080      * Atualiza a tela
081      @see java.util.Observer#update(java.util.Observable, java.lang.Object)
082      @param o Objeto que sofreu uma atualização
083      @param arg Argumento passado pelo objeto para seus observadores
084      */
085     public void update(Observable o, Object arg) {
086         if(arg instanceof Integer) {
087             //Seta o valor do progresso
088             barradeprogresso.setValue( ((Integerarg).intValue());
089             texto.setTextString.valueOf(((Integerarg).intValue()));
090         else if(arg instanceof Boolean) {
091             if( ((Booleanarg).booleanValue() ) {
092                 barradeprogresso.setValue(0);
093                 label.setText("Processo finalizado!");
094             }
095         }
096     }
097     
098     /**
099      * Instancia e mostra a tela do usuário
100      @param args Parâmetros da linha de execução
101      */
102     public static void main(String[] args) {
103         new classeGUI().show();
104     }
105 }



Vamos estudar as partes importantes:

Para executar nosso processo registramos o listener e chamamos o método executaProcesso, o código

1 private void executaProcesso() {
2     if(processo==null) { //Instancia a thread SE não existir uma
3         processo = new Thread(new classeProcesso(this));
4         processo.start();
5     else {
6         System.out.println("O processo ainda está em execução");
7     }    
8 }


primeiro verifica se o processo existe, caso exista é porque o botão foi clicado e nosso processo está sendo executado, imprimimos uma mensagem no console. Caso não exista criamos uma thread passando para o construtor a própria tela para que o processo possa nos registrar como observadores.

O método update é invocado a cada vez que o objeto observado é marcado como alterado. O código

01 public void update(Observable o, Object arg) {
02     if(arg instanceof Integer) {
03         //Seta o valor do progresso
04         barradeprogresso.setValue( ((Integerarg).intValue());
05         texto.setTextString.valueOf(((Integerarg).intValue()));
06     else if(arg instanceof Boolean) {
07         if( ((Booleanarg).booleanValue() ) {
08             barradeprogresso.setValue(0);
09             label.setText("Processo finalizado!");
10         }
11     }
12 }


Caso receba um objeto do tipo Integer como arg apenas atualiza a barra de progresso e o texto na caixa de texto, caso receba o Boolean e este seja true significa que o processo acabou e avisamos o usuário.
É neste método que está o segredo, deve-se fazer um planejamento no modo como serão notificadas as diferentes alterações para não impactar na performance (adianto que esse ai não é o ideal) e ainda assim ser significativo para a interface, aqui podemos aplicar outros patterns ou definirmos algo que melhor se encaixe no funcionamento do processo e da interface.

Uma possibilidade é disponibilizarmos no objeto do processo alguns métodos para melhorar a notificação, após a tela ser notificada ela pergunta ao objeto sobre seu status. (só uma idéia:)

Resultado
O resultado deste exemplo é uma tela que mantém o usuário informado sobre o andamento do processo, mas esta abordagem permite tirarmos melhor proveito da interface com o usuário, é claro que exige também um cuidado especial de controle das açoes do usuário enquanto nosso processo roda (no exemplo existe um controle simples, lembra?) !



Conclusão
Esta não é a única solução para o problema, nem mesmo a melhor pois há limitações por se utilizar a herança para o objeto Observable (sim existem técnicas para transpor essa limitação) mas talvez seja a mais simples e mais rápida de se implementar sendo com isso muito útil para interfaces simples baseadas em caixas de diálogos ou outras aplicações.
É possível apartir deste conceito construir modelos mais complexos e completos, caso tenha alguma dúvida pesquise sobre o assunto e poste as dúvidas aqui no GUJ.

Até breve!


Copyright © 2002-2006 GUJ | Todas as marcas e marcas registradas que aparecem no GUJ são de propriedade de seus respectivos donos