Le bloc notes Java

Le blog note Java

L'architecture MVC revisitée

L'architecture MVC (Model-View-Controller) est à la base de Swing. Elle apporte souplesse et puissance dans le codage d'interface graphique. Pourtant, quand il s'agit de l'appliquer à une interface graphique réelle, les choses se gâtent. Voici quelques solutions pour simplifier le codage d'interfaces graphiques en Java.

Rappel sur l'architecture MVC

L'architecture MVC est originaire du modèle de conception (design pattern) du même nom hérité du langage Smalltalk.
Cette architecture décompose la gestion de l'interface graphique en trois parties:

Les avantages de l'architecture MVC

Pourquoi utiliser l'architecture MVC?

    1) Isoler le modèle de la vue et du contrôleur

En isolant le modèle, il devient facile pour l'application de mettre à jour les données sans plus s'inquiéter de leur affichage.
L'interface graphique n'est plus qu'une couche graphique au dessus de l'application.
Elle peut évoluer librement sans imposer de modifications de l'application.
La structure de donnée peut changer tout en respectant le modèle utilisé par les composants graphiques. Nous verrons plus loin que c'est un peu théorique car les modèles Swing ne sont pas réutilisables tels quels.

    2) Isoler la vue et le contrôleur

La vue et le contrôleur ne se connaissent pas directement. Ils ne connaissent tout deux que le modèle.
Cela découple l'interface graphique du reste de l'application. Deux personnes peuvent travailler conjointement sans problèmes, le designer s'occupe de la vue (couche présentation), et le développeur s'occupe du traitement de données (couche métier).

    3) Multiplier les vues d'un même modèle

Plusieurs vue peuvent s'appliquer à un même modèle et présenter différemment la même information. Par exemple, une table et un arbre peuvent représenter les mêmes données différemment.

    4) Réutiliser les mêmes actions avec plusieurs composants

Si l'application contient plusieurs JTree, il est possible de rendre communes certaines action identiques (par exemple couper/copier/coller).

Les inconvénients de l'architecture MVC appliquée à Java

Voici les conséquences négatives que connaissent bien les utilisateurs de Swing (ou de SWT):

    1) Une débauche de classes

Prenons un exemple : supposons qu'une fenêtre graphique compte 20 composants graphiques actifs, 20 actions et 10 modèles.
Les composants graphiques principaux (non compris les menus, icons..) sont des JTree, JList, JComboBox, JFields..
Les actions sont des ActionListener, des SelectionListener, des TreeSelectionListener, des TreeModelListener, ..
Les modèles sont des DefaultTreeModel, TreeSelectionModel, ComboBoxModel..

Combien de classe faut-il pour gérer cette fenêtre?
Il faut 1 nouvelle classe par action ou par eventListener, souvent 1 nouvelle classe par modèle et quelques classes supplémentaires pour les composants que l'on veut spécialiser. Total, 40 nouvelles classes juste pour gérer une fenêtre moyennement complexe.
Conséquence, l'ouverture de la fenêtre est ralentie, la taille du code et des données augmente.
Le code est dispersé. Les trois parties de l'architecture sont enchevêtrées les unes dans les autres.
Le code est complexe à appréhender par un autre programmeur, exactement l'opposé du but initial de réutiliser facilement le code.

    2) Les modèles Swing ne sont pas réutilisables tels quel

Contrairement à la théorie un peu idéalisée, les modèles de donnée associés au composants Swing ne sont pas réutilisables tels quel.
Chaque modèle est dédicacé à un type de composant et de comportement. Le même modèle ne pourra pas être utilisé par plusieurs composants.
Par exemple un ListModel, un TableModel et un TreeNode n'ont rien en commun même s'ils contiennent la même liste de données.
Il n'y a pas d'indépendance totale du modèle par rapport à la vue et au contrôleur.

Il faut donc modifier (sous-classer) le modèle si l'interface graphique change.
Une solution consiste à créer un nouveau modèle pour chaque composants graphique qui appellerait un modèle de donnée neutre. Cela revient à ajouter une classe par modèle.

    3) Swing n'applique pas entièrement le modèle MVC

Tous les composants de Swing n'ont pas de modèle associé : les JLabel n'ont pas de modèle pour le texte et l'icon, les JSplitPane n'ont pas de modèle pour la position.. Ces composants ne peuvent pas être mis à jour depuis un eventListener en utilisant uniquement un modèle.
Donc, le contrôleur doit avoir accès à au moins une partie des composants graphiques de la vue.

   4) La réutilisation des actions est problématique

Si deux composants utilisent la même action, cela oblige l'action soit à être totalement neutre vis à vis du composant (aucune customisation d'un composant par rapport à l'autre) soit à identifier la source de l'évènement.
Identifier la source permet de traiter l'action spécifiquement ce qui revient à rompre l'architecture MVC (la vue et le contrôleur ne sont plus indépendants).

Comment simplifier l'interface graphique

Nous l'avons vu, le contrôleur et la vue sont difficiles à isoler l'un de l'autre. Une solution simple consiste à renoncer à cette isolation entre le  contrôleur et la vue. Ainsi, le contrôleur peut directement modifier un composant graphique s'il n'y a pas d'autres solutions (pas de modèles).
La vue, elle, initialise ces composants directement avec les eventListener du contrôleur.

Pour supprimer la création d'une classe à chaque gestionnaire d'évènement, on utilise une instance de classe Trampoline.
L'objet trampoline fait office d'intermédiaire entre l'évènement et l'appel d'une méthode du contrôleur. Il existe diverses implémentations de trampoline. Les unes basées sur les Proxy (voir EventHandler) et les autres sur la réflexion (voir la classe TrampolineAction décrite plus loin).
Nous allons voir un exemple d'interface graphique codée en utilisant ces solutions.

Un exemple d'interface graphique

L'exemple suivant utilise 5 composants, 5 listeners (Action, ChangeListener, WindowListener) et 1 modèle.
Le programme est composé de trois classes internes: Controller, Model et View.
La classe Controller possède une méthode par évènement à gérer. Chacun de ces évènements est appelé par une instance de
classe trampoline crée dynamiquement.
La classe TrampolineAction gère les évènements Action qui sont un peu particuliers. Elle étend la classe AbstractAction qui contient des champs de description pour le texte, l'icon et différents autres paramètres de l'action.
La classe EventHandler gère tous les autres évènements possibles. A noter que si le gestionnaire d'évènement est composé de plusieurs méthodes comme par exemple le WindowListener (composé de 7 méthodes), tous ces évènements aboutissent à la même méthode du contrôleur. Il faut alors inspecter l'évènement pour connaître son type.

Les classes internes Model et View ne sont pas indispensables car elles ne contiennent que des données, mais elles fixent mieux les idées.

package example;

import java.awt.BorderLayout;
import java.awt.Container;
import java.awt.event.ActionEvent;
import java.awt.event.WindowEvent;
import java.awt.event.WindowListener;
import java.beans.EventHandler;

import javax.swing.Action;
import javax.swing.DefaultBoundedRangeModel;
import javax.swing.Icon;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JSlider;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;

public final class MyForm {

    public final class Controller {

        Action actionMin = getAction("onButtonMin", "Min", null);

        Action actionMax = getAction("onButtonMax", "Max", null);

        Action closeAction = getAction("onClose", "Exit", null);

        ChangeListener changeListener = getChangeListener("onChange");

        WindowListener windowListener = getWindowListener("onWindowEvent");

        private Action getAction(final String command, final String name,
            Icon icon) {
            return new TrampolineAction(command, name, icon, this);
        }

        private ChangeListener getChangeListener(String method) {
            return (ChangeListener) EventHandler.create(ChangeListener.class,
                this, method, "");
        }

        private WindowListener getWindowListener(String method) {
            return (WindowListener) EventHandler.create(WindowListener.class,
                this, method, "");
        }

        public void onButtonMin(ActionEvent e) {
            model.boundedRangeModel.setValue(model.boundedRangeModel
                .getMinimum());
        }

        public void onButtonMax(ActionEvent e) {
            model.boundedRangeModel.setValue(model.boundedRangeModel
                .getMaximum());
        }

        public void onChange(ChangeEvent event) {
            view.label.setText("" + model.boundedRangeModel.getValue());
        }

        public void onClose(ActionEvent e) {
            view.frame.dispose();
        }

        public void onWindowEvent(WindowEvent e) {
            if (e.getID()==WindowEvent.WINDOW_ACTIVATED) {
                view.label.setText("Window Activated");
            }
        }
    }

    private final class Model {
        DefaultBoundedRangeModel boundedRangeModel = new DefaultBoundedRangeModel();
    }

    private final class View {
        JFrame frame;

        JLabel label;

        private void createAndShowGUI() {

            frame = new JFrame("Form Demo");
            frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
            frame.addWindowListener(controller.windowListener);

            label = new JLabel(" ");
            JButton btMax = new JButton(controller.actionMax);
            JButton btMin = new JButton(controller.actionMin);
            JButton closeBouton = new JButton(controller.closeAction);
            JSlider slider = new JSlider(model.boundedRangeModel);
            slider.addChangeListener(controller.changeListener);
           
            Container container=frame.getContentPane();
            container.add(slider, BorderLayout.NORTH);
            container.add(btMax, BorderLayout.WEST);
            container.add(btMin, BorderLayout.CENTER);
            container.add(closeBouton, BorderLayout.EAST);
            container.add(label, BorderLayout.SOUTH);
           
            frame.pack();
            frame.setVisible(true);
        }
    }

    private Controller controller;

    private Model model;

    private View view;

    public static void main(String[] args) {
        javax.swing.SwingUtilities.invokeLater(new Runnable() {
            public void run() {
                new MyForm().view.createAndShowGUI();
            }
        });
    }

    public MyForm() {
        super();
        model = new Model();
        controller = new Controller();
        view = new View();
    }
}

Une classe trampoline spécialisée pour les Actions

La classe TrampolineAction permet d'appeler une des méthodes du contrôleur quand une action se produit (après un appel de la méthode actionPerformed). L'exemple précédent utilise cette classe pour gérer les actions.
La méthode est initialisé au dernier moment pour accélérer la mise en route. Il faut compter 15 ms au premier appel et quelques us aux appels suivants.

package example;

import java.awt.event.ActionEvent;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.Icon;

/**
 * The <code>TrampolineAction</code> call the command method of the controller
 * when the actionPerformed method is invoked.
 */
public class TrampolineAction extends AbstractAction {

    private final Object controller;

    private transient Method method;

    /*
     * @see java.awt.event.ActionListener#actionPerformed(java.awt.event.ActionEvent)
     */
    public void actionPerformed(ActionEvent e) {
        try {
            if (method == null) {
                method = controller.getClass().getMethod(
                    (String) getValue(Action.ACTION_COMMAND_KEY), new Class[] {
                        ActionEvent.class
                    });
            }
            method.invoke(controller, new Object[] {
                e
            });
        } catch (NoSuchMethodException ex1) {
            throw new RuntimeException(ex1);
        } catch (InvocationTargetException ex2) {
            throw new RuntimeException(ex2.getTargetException());
        } catch (Exception ex3) {
            throw new RuntimeException(ex3);
        }
    }

    /**
     * TrampolineAction constructor.
     * @param command method name
     * @param name action name
     * @param icon action icon
     * @param controller controller object invoked on actionPerformed event.
     */
    public TrampolineAction(String command, String name, Icon icon,
        final Object controller) {
        super(name, icon);
        this.putValue(Action.ACTION_COMMAND_KEY, command);
        this.controller = controller;
    }
}

En savoir plus sur le sujet

sujet

liens

MVC Model-View-Controller Pattern, MVC meets Swing
The Java Tutorial
Différentes sortes de trampolines Asserting Control Over the GUI
La classe EventHandler
Class EventHandler, Using EventHandler for event listening, Proposal to reduce repetition with Swing and SWT listeners
MVC avec EventListener Patterns for Java Events
MVC avec la reflexion (method) Generating Event Listeners Dynamically, Putting Reflection to Work
MVC avec Dynamic Proxy Using Dynamic Proxies to Generate Event Listeners Dynamically, The Dynamic Proxy API, Explore the Dynamic Proxy API, Dynamic Proxy Classes, EventInitializer librairie
MVC avec Observer/Observable pour les nostalgiques Implementing The Model-View-Controller Paradigm using Observer and Observable, Building Graphical User Interfaces with the MVC Pattern

Le bilan

Le découpage en trois sous-classes isole clairement les trois parties de l'architecture MVC.
On voit qu'il est inutile de créer de nouvelles classes pour ajouter de nouveaux évènements.
Fini les spaghettis, cette implémentation est plus facile à gérer que les traditionnelles classes anonymes.
En bonus, le programme démarre plus vite.

Même si les interfaces graphiques sont plus faciles à coder en suivant ce guide, Il reste encore certains problèmes inhérents au framework Swing: les modèles ne sont pas réutilisables, la vue et le contrôleur ne sont pas isolés.


Ce site est mis à disposition sous licence Creative Commons. Copyright © 2004,2005 Louis Cova. Ecrivez-moi: