Sablonul arhitectural Model-View-Controler

Aplicaţiile complexe prezintă utilizatorului informaţii diverse sub diferite forme. În construirea arhitecturii aplicaţiilor trebuie ţinut cont de aceste aspecte. Unul dintre cele mai cunoscute modele folosite în construirea aplicaţiilor cu interfeţe grafice este şablonul Model–View-Controller (MVC).

MVC descompune obiectele aplicaţiei în 3 categorii:

  • Model: menţinea starea curentă a aplicaţiei, a unei părţi a aplicaţiei sau a unui set de date. De asemenea modelul poate desfăşura anumite acţiuni. Această componentă nu are nici o informaţie cu privire la reprezentarea datelor (modul în care informaţiile vor fi afişate către utilizator). Această componentă oferă metode prin intermediul cărora modelul poate fi interogat cu privire la starea curenta, sau starea curentă poate fi modificată. Un model trebuie să ofere un mecanism prin care componente de tip View să poată fi înregistrate în cadrul lui şi mecanisme prin care componentele View înregistrate să fie notificate atunci când starea modelului s-a modificat.
  • View: reprezinţă componentele care sunt interesate de starea curentă a aplicaţiei – în mod uzual aceste componente sunt componente grafice care reflectă starea curentă a unui model. Un model poate avea înregistrate mai multe View-uri.
  • Controller: procesează şi răspunde la evenimente – în mod uzual controlerul preia acţiunile utilizatorului şi acţionează asupra componentei Model sau asupra componentei View.

Diagrama Model-View-Controler preluată din documentaţia oficială Java Sun este prezentată în figura 1.

Figura 1. Arhitectura Model-View-Controller

Un eveniment captat de controler determină schimbarea componentei Model sau a componentei View. De fiecare dată când controlerul schimbă starea componentei Model toate componentele View sunt notificate automat. În mod similar atunci când controlerul schimbă o componentă View, aceasta extrage datele necesare din cadrul modelului pentru a fi afişate.

Implementare

Pentru implementarea modelului de tip Model-View-Controler se recomandă folosirea clasei java.util.Observable şi a interfeţei java.util.Observer. Aceste două componente permit realizarea unui mecanism de notificare automată a obiectelor de tip Observer în momentul în care o schimbare se produce în cadrul unui obiect de tip Observable. Observatorii trebuie să implementeze interfaţa Observer şi să se înregistreze în cadrul obiectelor de tip Observable ( prin apelarea metodei addObserver(Observer o)) pentru a primi notificări . În momentul în care un obiect de tip Observable s-a schimbat se apelează metoda setChanged() pentru a marca acest fapt. În momentul în care se doreşte anunţarea observatorilor cu privire la schimbarea stării obiectului observat se apelează metoda notifyObservers().

Se implementează clasa de tip Model. Aceasta va trebui să extindă java.util.Observable. În cazul de fată observatorii sunt componente de tip View. În momentul în care starea modelului s-a schimbat acesta va trebui să apeleze setChanged() şi notifyObserver(), pentru a notifica observatorii.

Se implementează unul sau mai multe componente View. Fiecare View va trebui să implementeze interfaţa java.util.Observer. Al doilea argument din cadrul metodei update() permite transmiterea de infromaţii suplimentare către observator.

interface Observer
{              void update(Observable t, Object o);       
}

Odată implementată această interfaţă, componentele de tip View trebuiesc înregistrate în cadrul modelului ca şi observatori, astfel încât atunci când un eveniment s-a produs acestea să fie notificate.

Exemplu aplicatie 1

În continuare este prezentat un exemplu de aplicaţie ce foloseşte mecanismul Model-View-Controller. Este realizată o aplicaţie ce simulează un termometru care citeşte temeperaturi din mediul înconjurător. Aceste temperaturi sunt afişate în două moduri: atât în mod text cât şi în mod grafic. Aplicaţia permite activarea şi dezactivarea termometrului.

Figura 2. Interfaţa grafică a aplicaţiei Termometru.

Modelul este reprezentat de o clasă de tip fir de execuţie care generează temperaturi aleatoare într-un anumit interval. După cum se observă modelul nu deţine nici o informaţie cu privire la modul în care datele vor fi prezentate către utilizator.

import java.util.Observable; 
public class Thermometer extends Observable implements Runnable {
      static int MAX_VALUE=100;
      static int MIN_VALUE=0;
      double temp=30;
      Thread t;
      boolean active = true;
      boolean paused = false;
 
      public void start(){
            if(t==null){
                  t = new Thread(this);
                  t.start();
            }
      }
 
      public void run(){
            while(active){
                  if(paused){
                        synchronized(this){
                              try {
                                    wait();
                              } catch (InterruptedException e) {
                                    e.printStackTrace();
                              }
                        }//.sync
                  }//.if
 
                  double d = Math.random()*4;
                  double x = Math.random();
                  if(x<0.5) d = -1*d;
 
                  if(temp+d<MAX_VALUE&&temp+d>MIN_VALUE){
                        temp= temp+d;
                        this.setChanged();
                        this.notifyObservers();
                  }
 
                  try {Thread.sleep(1000);} catch (InterruptedException e) {}
            }//.while
      }//.run
 
      public void setPause(boolean p){
            synchronized (this) {
                  if(p==true){
                        paused = true;
                  }else{
                        paused = false;
                        notify();
                  }
            }
      }
 
      public double getTemperature(){
            return temp;
      }
 
      public boolean isPaused() {
            return paused;
      }    
}

Clasele de tip View sunt responsabile cu afişarea pe ecran a informaţiilor şi preluarea de la utilizator a eventualelor acţiuni. În momentul în care un eveniment se produce în cadrul modelului (starea internă a acestuia se schimbă) componentele de tip View sunt anunţate prin apelarea metodei update(Observable o, Object arg). Primul argument al metodei update() conţine referinţă către obiectul de tip model care a generat evenimentul.

import java.awt.FlowLayout;
import java.util.Observable;
import java.util.Observer;
import javax.swing.*;
 
public class TemperatureTextView extends JPanel implements Observer{
 
      JTextField jtfTemp;
      JLabel jtlTemp;
      JButton action;
 
      TemperatureTextView(){
            this.setLayout(new FlowLayout());
            jtfTemp = new JTextField(20);
            jtlTemp = new JLabel("Temperature");
            action = new JButton("Enable-Disable");
            add(action);add(jtlTemp);add(jtfTemp);
      }
 
      public void update(Observable o, Object arg) {
            String s = ""+((Thermometer)o).getTemperature();
            jtfTemp.setText(s);
      }
 
      public void addEnableDisableListener(TemperatureController.EnableDisableListener listener) {
            action.addActionListener(listener);
      }
}
 
import java.awt.*;
import java.util.Observable;
import java.util.Observer;
import javax.swing.*;
 
public class TemperatureCanvasView extends JPanel implements Observer{
 
      private static final int width = 20;
      private static final int top = 20;
      private static final int left = 100;
      private static final int right = 250;
      private static final int height = 200;
 
      private double crtTemp;
 
      public void paintComponent(Graphics g){
 	super.paintComponent(g);
            g.setColor(Color.black);
            g.drawRect(left,top, width, height);
            g.setColor(Color.red);
            g.fillOval(left-width/2, top+height-width/3,width*2, width*2);
            g.setColor(Color.black);
            g.drawOval(left-width/2, top+height-width/3,width*2, width*2);
            g.setColor(Color.white);
            g.fillRect(left+1,top+1, width-1, height-1);
            g.setColor(Color.red);
            long redtop = (long)(height*(crtTemp-Thermometer.MAX_VALUE)/(Thermometer.MIN_VALUE-Thermometer.MAX_VALUE));
            g.fillRect(left+1, top + (int)redtop, width-1, height-(int)redtop);
            g.setColor(Color.BLUE);
      }
 
      public void update(Observable o, Object arg) {
            crtTemp = ((Thermometer)o).getTemperature();
            repaint();
      }
}

Clasa de tip Controller este responsabilă cu realizarea legaturilor dintre View-uri şi Model şi interpretarea acţiunilor utilizatorului.

import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
 
public class TemperatureController  {
      Thermometer t;
      TemperatureTextView tview;
      public TemperatureController(Thermometer t, TemperatureTextView tview, TemperatureCanvasView tcanvasView){
            t.addObserver(tview);
            t.addObserver(tcanvasView);
            this.t = t;
            this.tview = tview;
 
            tview.addEnableDisableListener(new EnableDisableListener());
      }    
 
      class EnableDisableListener implements ActionListener{
 
            public void actionPerformed(ActionEvent e) {
                  t.setPause(!t.isPaused());
            }
 
      }
}

Punctul de start al aplicaţiei îl reprezintă clasa TemperatureApp în cadrul căreia se iniţializează obiectele de tip Model, View şi Controler şi de asemenea se iniţializează interfaţa grafică.

import java.awt.BorderLayout;
import java.awt.Dimension;
import javax.swing.*;
 
public class TemperatureApp extends JFrame{
 
      TemperatureApp(TemperatureTextView tview, TemperatureCanvasView tcanvasView){
            setLayout(new BorderLayout());
            tcanvasView.setPreferredSize(new Dimension(300,300));
            add(tview,BorderLayout.NORTH);
            add(tcanvasView,BorderLayout.CENTER);
            pack();
            setVisible(true);
      }
 
      public static void main(String[] args) {
            Thermometer t = new Thermometer();
            t.start();
 
            TemperatureCanvasView tcanvasView = new TemperatureCanvasView();
            TemperatureTextView tview = new TemperatureTextView();
            TemperatureController tcontroler = new TemperatureController(t,tview,tcanvasView);
 
            new TemperatureApp(tview,tcanvasView);
      }
}

Exemplu aplicatie 2

În cadrul unei aplicaţii pot exista mai multe componente de tip Model, View şi Controller.

O variantă a şablonului Model-View-Controller este cea în care componentele View şi Controller sunt integrate în cadrul aceleiaşi clase.

import java.awt.*;
import java.awt.event.*;
import java.math.BigInteger;
import javax.swing.*;
 
public class Calc {
      public static void main(String[] args) {
             JFrame presentation = new CalcViewController();
           presentation.setVisible(true);
      }
}
 
 
class CalcViewController extends JFrame {
    private static final String INITIAL_VALUE = "1";
 
    //... The Model.
    private CalcModel  m_logic;
 
    private JTextField m_userInputTf = new JTextField(5);
    private JTextField m_totalTf     = new JTextField(20);
    private JButton    m_multiplyBtn = new JButton("Multiply");
    private JButton    m_clearBtn    = new JButton("Clear");
 
    /** Constructor */
    CalcViewController() {
        //... Set up the logic
        m_logic = new CalcModel();
        m_logic.setValue(INITIAL_VALUE);
 
        //... Initialize components
        m_totalTf.setText(m_logic.getValue());
        m_totalTf.setEditable(false);
 
        //... Layout the components.       
        JPanel content = new JPanel();
        content.setLayout(new FlowLayout());
        content.add(new JLabel("Input"));
        content.add(m_userInputTf);
        content.add(m_multiplyBtn);
        content.add(new JLabel("Total"));
        content.add(m_totalTf);
        content.add(m_clearBtn);
 
        //... Add button listeners.
        m_multiplyBtn.addActionListener(new MultiplyListener());
        m_clearBtn.addActionListener(new ClearListener());
 
        //... finalize layout and set window parameters.
        this.setContentPane(content);
        this.pack();
        this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        this.setTitle("Simple Calc - Presentation-Model");
    }//end constructor
 
 
    //Inner classes
    /** When a multiplication is requested.
     *  1. Get the user input number.
     *  2. Call the model to mulitply by this number.
     *  3. Get the result from the Model.
     *  4. Set the Total textfield to this result.
     * If there was an error, display it in a JOptionPane.
     */
    class MultiplyListener implements ActionListener {
        public void actionPerformed(ActionEvent e) {
            String userInput = "";
            try {
                userInput = m_userInputTf.getText();
                m_logic.multiplyBy(userInput);
                m_totalTf.setText(m_logic.getValue());
            } catch (NumberFormatException nfex) {
                JOptionPane.showMessageDialog(CalcViewController.this,
                                      "Bad input: '" + userInput + "'");
            }
        }
    }//end inner class MultiplyListener
 
 
    //////////////////////////////////////////// inner class ClearListener
    /**  1. Reset model.
     *   2. Put model's value into Total textfield.
     */   
    class ClearListener implements ActionListener {
        public void actionPerformed(ActionEvent e) {
            m_logic.reset();
            m_totalTf.setText(m_logic.getValue());
        }
    }
}
 
class CalcModel {
 
    private static final String INITIAL_VALUE = "1";
    private BigInteger m_total; 
 
    public CalcModel() {
        reset();
    }
 
    public void reset() {
        m_total = new BigInteger(INITIAL_VALUE);
    }
 
    public void multiplyBy(String operand) {
        m_total = m_total.multiply(new BigInteger(operand));
    }
 
    public void setValue(String value) {
        m_total = new BigInteger(value);
    }
 
    public String getValue() {
        return m_total.toString();
    }
}