Ce chapitre va vous présenter un des design patterns les plus connus : MVC. Il va vous apprendre à découper vos codes en trois parties : modèle, vue et contrôleur. C'est un pattern composé, ce qui signifie qu'il est constitué d'au moins deux patterns (mais rien n'empêche qu'il y en ait plus).
Nous allons voir cela tout de suite, inutile de tergiverser plus longtemps !
Premiers pas
Dans les chapitres précédents, nous avons agi de la manière suivante :
- mise en place d'une situation ;
- examen de ce que nous pouvions faire ;
- découverte du pattern.
Ici, nous procéderons autrement : puisque le pattern MVC est plus complexe à aborder, nous allons entrer directement dans le vif du sujet. Le schéma présenté à la figure suivante en décrit le principe ; il ne devrait pas être étranger à ceux d'entre vous qui auraient déjà fait quelques recherches concernant ce pattern.
Avant d'expliquer ce schéma, nous devons faire le point sur ce que sont réellement ces trois entités.
La vue
Ce que l'on nomme « la vue » est en fait une IHM. Elle représente ce que l'utilisateur a sous les yeux. La vue peut donc être :
- une application graphique
Swing
,AWT
,SWT
pour Java (Form
pour C#…) ; - une page web ;
- un terminal Linux ou une console Windows ;
- etc.
Le modèle
Le modèle peut être divers et varié. C'est là que se trouvent les données. Il s'agit en général d'un ou plusieurs objets Java. Ces objets s'apparentent généralement à ce qu'on appelle souvent « la couche métier » de l'application et effectuent des traitements absolument transparents pour l'utilisateur. Par exemple, on peut citer des objets dont le rôle est de gérer une ou plusieurs tables d'une base de données. En trois mots, il s'agit du cœur du programme !
Dans le chapitre précédent, nous avons confectionné un jeu du pendu. Dans cette application, notre fenêtre Swing
correspond à la vue et l'objet Model
correspond au modèle.
Le contrôleur
Cet objet - car il s'agit aussi d'un objet - permet de faire le lien entre la vue et le modèle lorsqu'une action utilisateur est intervenue sur la vue. C'est cet objet qui aura pour rôle de contrôler les données.
Maintenant que toute la lumière est faite sur les trois composants de ce pattern, je vais expliquer plus précisément la façon dont il travaille.
Afin de travailler sur un exemple concret, nous allons reprendre notre calculatrice issue d'un TP précédent.
Dans une application structurée en MVC, voici ce qu'il peut se passer :
- l'utilisateur effectue une action sur votre calculatrice (un clic sur un bouton) ;
- l'action est captée par le contrôleur, qui va vérifier la cohérence des données et éventuellement les transformer afin que le modèle les comprenne. Le contrôleur peut aussi demander à la vue de changer ;
- le modèle reçoit les données et change d'état (une variable qui change, par exemple) ;
- le modèle notifie la vue (ou les vues) qu'il faut se mettre à jour ;
- l'affichage dans la vue (ou les vues) est modifié en conséquence en allant chercher l'état du modèle.
Je vous disais plus haut que le pattern MVC était un pattern composé : à ce stade de votre apprentissage, vous pouvez isoler deux patterns dans cette architecture. Le pattern observer se trouve au niveau du modèle. Ainsi, lorsque celui-ci va changer d'état, tous les objets qui l'observeront seront mis au courant automatiquement, et ce, avec un couplage faible !
Le deuxième est plus difficile à voir mais il s'agit du pattern strategy ! Ce pattern est situé au niveau du contrôleur. On dit aussi que le contrôleur est la stratégie (en référence au pattern du même nom) de la vue. En fait, le contrôleur va transférer les données de l'utilisateur au modèle et il a tout à fait le droit de modifier le contenu.
Ceux qui se demandent pourquoi utiliser le pattern strategy pourront se souvenir de la raison d'être de ce pattern : encapsuler les morceaux de code qui changent ! En utilisant ce pattern, vous prévenez les risques potentiels de changement dans votre logique de contrôle. Il vous suffira d'utiliser une autre implémentation de votre contrôleur afin d'avoir des contrôles différents.
Ceci dit, vous devez tout de même savoir que le modèle et le contrôleur sont intimement liés : un objet contrôleur pour notre calculatrice ne servira que pour notre calculatrice ! Nous pouvons donc autoriser un couplage fort entre ces deux objets.
Je pense qu'il est temps de se mettre à coder !
Le modèle
Le modèle est l'objet qui sera chargé de stocker les données nécessaires à un calcul (nombre et opérateur) et d'avoir le résultat. Afin de prévoir un changement éventuel de modèle, nous créerons le notre à partir d'un supertype de modèle : de cette manière, si un changement s'opère, nous pourrons utiliser les différentes classes filles de façon polymorphe.
Avant de foncer tête baissée, réfléchissons à ce que notre modèle doit être capable d'effectuer. Pour réaliser des calculs simples, il devra :
- récupérer et stocker au moins un nombre ;
- stocker l'opérateur de calcul ;
- calculer le résultat ;
- renvoyer le résultat ;
- tout remettre à zéro.
Très bien : voila donc la liste des méthodes que nous trouverons dans notre classe abstraite.
Comme vous le savez, nous allons utiliser le pattern observer afin de faire communiquer notre modèle avec d'autres objets. Il nous faudra donc une implémentation de ce pattern ; la voici, dans un package com.sdz.observer
.
Observable.java
1 2 3 4 5 6 7 | package com.sdz.observer; public interface Observable { public void addObserver(Observer obs); public void removeObserver(); public void notifyObserver(String str); } |
Observer.java
1 2 3 4 5 | package com.sdz.observer; public interface Observer { public void update(String str); } |
Notre classe abstraite devra donc implémenter ce pattern afin de centraliser les implémentations. Puisque notre supertype implémente le pattern observer, les classes héritant de cette dernière hériteront aussi des méthodes de ce pattern !
Voici donc le code de notre classe abstraite que nous placerons dans le package
com.sdz.model
.
AbstractModel.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 | package com.sdz.model; import java.util.ArrayList; import com.sdz.observer.Observable; import com.sdz.observer.Observer; public abstract class AbstractModel implements Observable{ protected double result = 0; protected String operateur = "", operande = ""; private ArrayList<Observer> listObserver = new ArrayList<Observer>(); //Efface public abstract void reset(); //Effectue le calcul public abstract void calcul(); //Affichage forcé du résultat public abstract void getResultat(); //Définit l'opérateur de l'opération public abstract void setOperateur(String operateur); //Définit le nombre à utiliser pour l'opération public abstract void setNombre(String nbre) ; //Implémentation du pattern observer public void addObserver(Observer obs) { this.listObserver.add(obs); } public void notifyObserver(String str) { if(str.matches("^0[0-9]+")) str = str.substring(1, str.length()); for(Observer obs : listObserver) obs.update(str); } public void removeObserver() { listObserver = new ArrayList<Observer>(); } } |
Ce code est clair et simple à comprendre. Maintenant, nous allons créer une classe concrète héritant de AbstractModel
.
Voici la classe concrète que j'ai créée.
Calculator.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 | package com.sdz.model; import com.sdz.observer.Observable; public class Calculator extends AbstractModel{ //Définit l'opérateur public void setOperateur(String ope){ //On lance le calcul calcul(); //On stocke l'opérateur this.operateur = ope; //Si l'opérateur n'est pas = if(!ope.equals("=")){ //On réinitialise l'opérande this.operande = ""; } } //Définit le nombre public void setNombre(String result){ //On concatène le nombre this.operande += result; //On met à jour notifyObserver(this.operande); } //Force le calcul public void getResultat() { calcul(); } //Réinitialise tout public void reset(){ this.result = 0; this.operande = "0"; this.operateur = ""; //Mise à jour ! notifyObserver(String.valueOf(this.result)); } //Calcul public void calcul(){ //S'il n'y a pas d'opérateur, le résultat est le nombre saisi if(this.operateur.equals("")){ this.result = Double.parseDouble(this.operande); } else{ //Si l'opérande n'est pas vide, on calcule avec l'opérateur de calcul if(!this.operande.equals("")){ if(this.operateur.equals("+")) this.result += Double.parseDouble(this.operande); if(this.operateur.equals("-")) this.result -= Double.parseDouble(this.operande); if(this.operateur.equals("*")) this.result *= Double.parseDouble(this.operande); if(this.operateur.equals("/")){ try{ this.result /= Double.parseDouble(this.operande); }catch(ArithmeticException e){ this.result = 0; } } } } this.operande = ""; //On lance aussi la mise à jour ! notifyObserver(String.valueOf(this.result)); } } |
Voilà, notre modèle est prêt à l'emploi ! Nous allons donc continuer à créer les composants de ce pattern.
Le contrôleur
Celui-ci sera chargé de faire le lien entre notre vue et notre modèle. Nous créerons aussi une classe abstraite afin de définir un supertype de variable pour utiliser, le cas échéant, des contrôleurs de façon polymorphe.
Que doit faire notre contrôleur? C'est lui qui va intercepter les actions de l'utilisateur, qui va modeler les données et les envoyer au modèle. Il devra donc :
- agir lors d'un clic sur un chiffre ;
- agir lors d'un clic sur un opérateur ;
- avertir le modèle pour qu'il se réinitialise dans le cas d'un clic sur le bouton
reset
; - contrôler les données.
Voilà donc notre liste de méthodes pour cet objet. Cependant, puisque notre contrôleur doit interagir avec le modèle, il faudra qu'il possède une instance de notre modèle.
Voici donc le code source de notre superclasse de contrôle.
AbstractControler.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 | package com.sdz.controler; import java.util.ArrayList; import com.sdz.model.AbstractModel; public abstract class AbstractControler { protected AbstractModel calc; protected String operateur = "", nbre = ""; protected ArrayList<String> listOperateur = new ArrayList<String>(); public AbstractControler(AbstractModel cal){ this.calc = cal; //On définit la liste des opérateurs //Afin de s'assurer qu'ils sont corrects this.listOperateur.add("+"); this.listOperateur.add("-"); this.listOperateur.add("*"); this.listOperateur.add("/"); this.listOperateur.add("="); } //Définit l'opérateur public void setOperateur(String ope){ this.operateur = ope; control(); } //Définit le nombre public void setNombre(String nombre){ this.nbre = nombre; control(); } //Efface public void reset(){ this.calc.reset(); } //Méthode de contrôle abstract void control(); } |
Nous avons défini les actions globales de notre objet de contrôle et vous constatez aussi qu'à chaque action dans notre contrôleur, celui-ci invoque la méthode control()
. Celle-ci va vérifier les données et informer le modèle en conséquence.
Nous allons voir maintenant ce que doit effectuer notre instance concrète. Voici donc, sans plus tarder, notre classe.
CalculetteControler.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | package com.sdz.controler; import com.sdz.model.AbstractModel; public class CalculetteControler extends AbstractControler { public CalculetteControler(AbstractModel cal) { super(cal); } public void control() { //On notifie le modèle d'une action si le contrôle est bon //-------------------------------------------------------- //Si l'opérateur est dans la liste if(this.listOperateur.contains(this.operateur)){ //Si l'opérateur est = if(this.operateur.equals("=")) this.calc.getResultat(); //On ordonne au modèle d'afficher le résultat //Sinon, on passe l'opérateur au modèle else this.calc.setOperateur(this.operateur); } //Si le nombre est conforme if(this.nbre.matches("^[0-9.]+$")) this.calc.setNombre(this.nbre); this.operateur = ""; this.nbre = ""; } } |
Vous pouvez voir que cette classe redéfinit la méthode control()
et qu'elle permet d'indiquer les informations à envoyer à notre modèle. Celui-ci mis à jour, les données à afficher dans la vue seront envoyées via l'implémentation du pattern observer entre notre modèle et notre vue. D'ailleurs, il ne nous manque plus qu'elle, alors allons-y !
La vue
Voici le plus facile à développer et ce que vous devriez maîtriser le mieux… La vue sera créée avec le package javax.swing
. Je vous donne donc le code source de notre classe que j'ai mis dans le package com.sdz.vue
.
Calculette.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 | package com.sdz.vue; //CTRL + SHIFT + O pour générer les imports public class Calculette extends JFrame implements Observer{ private JPanel container = new JPanel(); String[] tab_string = {"1", "2", "3", "4", "5", "6", "7", "8", "9", "0", ".", "=", "C", "+", "-", "*", "/"}; JButton[] tab_button = new JButton[tab_string.length]; private JLabel ecran = new JLabel(); private Dimension dim = new Dimension(50, 40); private Dimension dim2 = new Dimension(50, 31); private double chiffre1; private boolean clicOperateur = false, update = false; private String operateur = ""; //L'instance de notre objet contrôleur private AbstractControler controler; public Calculette(AbstractControler controler){ this.setSize(240, 260); this.setTitle("Calculette"); this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); this.setLocationRelativeTo(null); this.setResizable(false); initComposant(); this.controler = controler; this.setContentPane(container); this.setVisible(true); } private void initComposant(){ Font police = new Font("Arial", Font.BOLD, 20); ecran = new JLabel("0"); ecran.setFont(police); ecran.setHorizontalAlignment(JLabel.RIGHT); ecran.setPreferredSize(new Dimension(220, 20)); JPanel operateur = new JPanel(); operateur.setPreferredSize(new Dimension(55, 225)); JPanel chiffre = new JPanel(); chiffre.setPreferredSize(new Dimension(165, 225)); JPanel panEcran = new JPanel(); panEcran.setPreferredSize(new Dimension(220, 30)); //Nous utiliserons le même listener pour tous les opérateurs OperateurListener opeListener = new OperateurListener(); for(int i = 0; i < tab_string.length; i++) { tab_button[i] = new JButton(tab_string[i]); tab_button[i].setPreferredSize(dim); switch(i){ case 11 : tab_button[i].addActionListener(opeListener); chiffre.add(tab_button[i]); break; case 12 : tab_button[i].setForeground(Color.red); tab_button[i].addActionListener(new ResetListener()); tab_button[i].setPreferredSize(dim2); operateur.add(tab_button[i]); break; case 13 : case 14 : case 15 : case 16 : tab_button[i].setForeground(Color.red); tab_button[i].addActionListener(opeListener); tab_button[i].setPreferredSize(dim2); operateur.add(tab_button[i]); break; default : chiffre.add(tab_button[i]); tab_button[i].addActionListener(new ChiffreListener()); break; } } panEcran.add(ecran); panEcran.setBorder(BorderFactory.createLineBorder(Color.black)); container.add(panEcran, BorderLayout.NORTH); container.add(chiffre, BorderLayout.CENTER); container.add(operateur, BorderLayout.EAST); } //Les listeners pour nos boutons class ChiffreListener implements ActionListener{ public void actionPerformed(ActionEvent e) { //On affiche le chiffre en plus dans le label String str = ((JButton)e.getSource()).getText(); if(!ecran.getText().equals("0")) str = ecran.getText() + str; controler.setNombre(((JButton)e.getSource()).getText()); } } class OperateurListener implements ActionListener{ public void actionPerformed(ActionEvent e) { controler.setOperateur(((JButton)e.getSource()).getText()); } } class ResetListener implements ActionListener{ public void actionPerformed(ActionEvent arg0) { controler.reset(); } } //Implémentation du pattern observer public void update(String str) { ecran.setText(str); } } |
Vous devez être à même de comprendre ce code, puisqu'il ressemble beaucoup à notre calculette réalisée dans le TP du chapitre correspondant. Vous constaterez que la vue contient le contrôleur (juste avant le constructeur de la classe).
Toutes nos classes sont à présent opérationnelles. Il ne nous manque plus qu'une classe de test afin d'observer le résultat. Elle crée les trois composants qui vont dialoguer entre eux : le modèle (données), la vue (fenêtre) et le contrôleur qui lie les deux. La voici :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | import com.sdz.controler.*; import com.sdz.model.*; import com.sdz.vue.Calculette; public class Main { public static void main(String[] args) { //Instanciation de notre modèle AbstractModel calc = new Calculator(); //Création du contrôleur AbstractControler controler = new CalculetteControler(calc); //Création de notre fenêtre avec le contrôleur en paramètre Calculette calculette = new Calculette(controler); //Ajout de la fenêtre comme observer de notre modèle calc.addObserver(calculette); } } |
Testez ce code : le tout fonctionne très bien ! Tous nos objets sont interconnectés et dialoguent facilement, comme le montre la figure suivante.
Comme vous connaissez la façon de travailler de ce pattern, nous allons décortiquer ce qui se passe.
Lorsque nous cliquons sur un chiffre :
- L'action est envoyée au contrôleur.
- Celui-ci vérifie si le chiffre est conforme.
- Il informe le modèle.
- Ce dernier est mis à jour et informe la vue de ses changements.
- La vue rafraîchit son affichage.
Lorsque nous cliquons sur un opérateur :
- L'action est toujours envoyée au contrôleur.
- Celui-ci vérifie si l'opérateur envoyé est dans sa liste.
- Le cas échéant, il informe le modèle.
- Ce dernier agit en conséquence et informe la vue de son changement.
- La vue est mise à jour.
Il se passera la même chose lorsque nous cliquerons sur le bouton reset
.
Nous aurions très bien pu faire la même chose sans le contrôleur !
Oui, bien sûr. Même sans modèle ! Rappelez-vous de la raison d'exister du design pattern : prévenir des modifications de codes ! Avec une telle architecture, vous pourrez travailler à trois en même temps sur le code : une personne sur la vue, une sur le modèle, une sur le contrôleur.
J'émets toutefois quelques réserves concernant ce pattern. Bien qu'il soit très utile grâce à ses avantages à long terme, celui-ci complique grandement votre code et peut le rendre très difficile à comprendre pour une personne extérieure à l'équipe de développement. Même si le design pattern permet de résoudre beaucoup de problèmes, attention à la « patternite aigüe » : son usage trop fréquent peut rendre le code incompréhensible et son entretien impossible à réaliser.
- Le pattern MVC est un pattern composé du pattern observer et du pattern strategy.
- Avec ce pattern, le code est découpé en trois parties logiques qui communiquent entre elles :
- Le modèle (données)
- La vue (fenêtre)
- Le contrôleur qui lie les deux.
- L'implémentation du pattern observer permet au modèle de tenir informés ses observateurs.
- L'implémentation du pattern strategy permet à la vue d'avoir des contrôles différents.
- Utiliser ce pattern permet de découpler trois acteurs d'une application, ce qui permet plus de souplesse et une maintenance plus aisée du code.