Programmation avancée
Programmation par objets
Un objet fusionne des données (caractéristiques structurelles ou variables) et des traitements (caractéristiques comportementales ou opérations) dans un tout dont le type est une classe (class). C'est la classe qui déclare les données et définit les opérations (on parle alors de méthodes).
Pour créer un objet, on crée un exemplaire de sa classe par une opération appelée instanciation (avec l'opérateur new). Dans la classe, une méthode du nom de la classe est utilisée à l'instanciation pour initialiser la valeur des variables de l'objet, on l'appelle un constructeur.
Le projet compteur illustre cette fusion données-traitements dans une classe Compteur (représentée ci-après en UML) ainsi que l'instanciation et l'utilisation d'un objet dans la classe EssaiCompteur qui illustre la structure d'un programme java avec le point d'entrée du programme défini dans une méthode appelée main.
La fiabilité du programme n'est pas assurée par la fusion données-traitements puisque les données des objets peuvent être manipulées directement par les programmes qui les utilisent. Mais on peut cacher (rendre privé - private noté - en UML) certaines caractéristiques structurelles ou comportementales (des sous-programmes utiles pour définir les opérations exposées). Cela empêche l'accès direct aux données cachées des objets ou le déclenchement des méthodes cachées. On parle de masquage (de l'implantation). Les autres caractéristiques sont alors qualifiées de publiques (public noté + en UML).
Si on permet l'obtention et/ou la modification de la valeur des données cachées par le biais de méthodes publiques dites informateurs (getters) et transformateurs (setters), on parle d'encapsulation. Cela permet de garantir la validité des données d'un objet (valeur dans leur intervalle de définition par exemple) parce que ces méthodes peuvent réaliser des traitements de contrôle avec l'accès proprement dit aux données.
Ce projet compteur illustre cette encapsulation dans une classe Compteur (représentée ci-après en UML) ainsi que l'instanciation et l'utilisation d'un objet dans la classe EssaiCompteur.
Le projet bonjour illustre la structure d'un programme java qui traite les arguments de la ligne de commande comme paramètres du point d'entrée du programme ainsi que :
- la possibilité de définir plusieurs méthodes de même nom (saluer de la classe Groupe fait appel à saluer de la classe Personne - on parle ici de délégation)
- l'héritage (technique de factorisation du code commun à plusieurs classes - ici VIP a des points communs avec Personne qui lui sont transmis par héritage)
- la compatibilité des types d'objets (VIP est compatible avec Personne puisqu'un VIP est une sorte de Personne).
Les caractéristiques privées d'une classe sont transmises à ses classes dérivées mais elles n'y sont pas utilisables directement (principe d'encapsulation). Pour transmettre une caractéristique cachée par héritage tout en permettant aux classes dérivées d'y accéder, on utilise la visibilité protégé (protected notée # en UML).
La qualification de static de la méthode main est expliquée plus loin (méthode de classe). Le tableau de chaînes de caractères en paramètre de la méthode permet de prendre en compte les arguments de la ligne de commande.
Essayer : java Bonjour Luc Maître:Yoda
Pour du logiciel de qualité
La qualité du logiciel est une appréciation globale basée sur de nombreux indicateurs. Elle tient compte de facteurs externes, directement observables par l'utilisateur, et de facteurs internes, observables par les développeurs lors des revues de code ou de la maintenance, qu'elle soit corrective ou évolutive.
La maintenance du logiciel est facilité par l'existence d'une documentation du code. Celle-ci peut-être intégrée au code sous forme de commentaires informels qui ne donnent pas de vision globale et obligent à lire tout le code. Une documentation séparée (au traitement de texte) est fastidieuse à rédiger et pratiquement jamais maintenue à jour. La solution est une documentation séparée, produite automatiquement par extraction de commentaires spéciaux intégrés au code comme dans le cas de JavaDoc pour les programmes écrits en Java. Une telle documentation prend du temps mais quand le code change, elle peut être mise à jour en même temps et sans trop d'effort puisqu'elle se trouve dans les fichiers modifiés.
La fiabilité du logiciel peut être obtenue par deux approches de programmation :
- la programmation défensive
- la programmation par contrat
Des assertions peuvent être ajoutées dans le code pour s'assurer de certaines conditions. L'avantage des assertions est que le logiciel peut être exécuté de 2 façons :
- avec vérification des assertions pendant sa mise au point
- sans vérification des assertions pour les utilisateurs finaux (l'exécution est performante car les tests ne sont pas effectués mais le logiciel étant au point, ce n'est plus nécessaire)
Le concept d'exception peut aussi être mis en œuvre pour rendre le logiciel robuste.
exemple 1 d'assertion exemple 2 d'assertion exemple d'exceptions exemple de programmation par contrat
La fiabilité du logiciel peut être vérifiée par des tests. En cas d'échec, les tests permettent de localiser les défauts et de les corriger. Les tests peuvent être automatisés, ce qui rend leur exécution plus rapide. C'est particulièrement utile pour s'assurer de la non-régression puisque cela permet de refaire tous les tests (ceux qui échouaient avant correction du code et aussi ceux qui réussissaient). JUnit est un cadriciel de test automatisé pour les programmes écrits en Java.
/!\ Tout programme est un assemblage de composants dont il faut tester séparément le bon fonctionnement avant de les assembler. On parle de test unitaire. Lorsqu'on teste le fonctionnement d'un ensemble de composants (2 ou plus), on parle de test d'intégration.
Certaines classes en utilisent d'autres pour fonctionner. Pour tester ces classes sans celles qu'elles utilisent, on remplace les classes utilisées par des simulacres ou doublures (mock objects). Il en existe différentes sortes : bouffon (dummy), substitut (fake), bouchon (stub)…
Exercice pour la prochaine séance
Définir une classe Date qui permette le succès de tous les tests automatisés de la classe TestDate. Ce travail est à rendre par courrier électronique sous l'intitulé "TP01 NFP121" à l'adresse "philippe.brutus (à) caensup.fr" pour le 14/02/2025 à 8h00 avec en pièce jointe le fichier "Date.java".
Dans le corrigé de cet exercice, la classe Date définit des opérations qui s'appliquent à des dates, c'est-à-dire à des instances (ou des représentants) de la classe Date :
- jour
- mois
- année
- jourPlus1
- moinsPlus1
- anneePlus1
…
Ce sont des méthodes d'instance.
Ces méthodes sont le seul moyen de connaitre ou modifier les valeurs des données d'une date (principe d'encapsulation). Cela garantit la validité de chaque donnée mais aussi la cohérence des données entre elles (le jour est cohérent avec le mois - pas de 31 avril, pas de 30 février, pas de 29 février en dehors des années bissextiles).
La classe Date définit aussi des opérations utilisables sans instance ou représentant de la classe :
- bissextile
- nombreDeJoursDuMois
Ce sont des méthodes de classe car on les appelle sur la classe elle-même. Ces méthodes sont qualifiées de statiques (static).
La méthode main est qualifiée de statique pour cette raison : on doit pouvoir l'appeler sans avoir eu à instancier un objet avant (ce qui ne pourrait être fait que par la méthode main elle-même !).
Les différentes versions d'essai de la méthode bissextile de la classe Date illustrent de quelles manières ont peut utiliser des méthodes de classe (import de la classe ou import statique de la méthode). (L'organisation des classes en paquets est vue ci-après).
Organisation des composants du logiciel
Les classes d'un programme sont regroupées dans des ensembles appelés paquets (package) qui peuvent contenir eux-mêmes des paquets, organisant ainsi les composants du logiciel en une structure hiérarchique.
Une classe déclarée publique dans le paquet qui la contient est visible et utilisable en dehors de ce paquet. Pour l'utiliser en dehors du paquet qui la contient, il faut indiquer où la trouver avec l'instruction import suivie du chemin d'accès à la classe. Ce chemin est une suite de noms de paquets séparés par le caractère ':', terminée par le nom de la classe.
Une classe qui n'est qualifiée ni de publique, ni de privée ou protégée est visible dans son paquet par les autres classes du paquet. Elle a la visibilité paquet (~ en UML).
Le projet bonjour met en œuvre une organisation en paquets des classes.
Abstraction
Dans le programme bonjour, un groupe contient des personnes. Le tableau dans lequel les membres du groupe sont consignés est déclaré comme un tableau de personnes. Pourtant, on peut y mettre des instances de la classe Personne et aussi des instances de la classe VIP. Cette compatibilité entre objets de types différents est due à l'héritage mais si un VIP est compatible avec une Personne, le contraire n'est pas vrai. En effet, VIP est une spécialisation de Personne alors que Personne est plus général que VIP. C'est pourquoi on parle de généralisation-spécialisation à propos d'héritage.
Il est donc possible de déclarer un type pour un objet et d'utiliser un objet d'un autre type à condition que le type déclaré soit le plus général. Avec l'héritage, cette généralisation peut s'exprimer dans des classes qu'il n'est pas concevable d'instancier. On parle de classe abstraite. C'est le cas par exemple dans une gestion de stock d'un magasin qui vend des aliments, des vêtements et d'autres choses encore qui ont en commun des données (référence, désignation, prix de vente, quantité en stock…) et des opérations (ajoute, retire…).
On peut créer des instances de Aliment, de Vetement… mais pas de Article. C'est une généralisation, un concept mais qui n'a pas d'exemplaires dans un stock. On dit que c'est une classe abstraite (abstract). Pourtant, c'est une généralisation compatible avec d'autres types d'objets qui peut servir dans une déclaration.
Remarque : Un concept abstrait n'est pas instanciable mais une classe non instanciable n'est pas toujours la représentation d'un concept abstrait. Dans les projets précédents, la classe qui définissait la méthode main était déclarée abstraite, non pas parce qu'elle représente un concept abstrait, mais parce qu'elle n'est pas instanciable.
Dans certains cas, une classe spécifie des opérations sans en donner de définition. On qualifie d'abstraites ces opérations. Par exemple, dans le domaine du déménagement, la classe Meuble contient une opération déplacer parce qu'un meuble peut être déplacé mais on ne sait pas dire comment le faire. Cela dépend du meuble. On pourra donner une définition à l'opération déplacer dans des spécialisations de meuble : Chaise, Table, Buffet… La classe Meuble représente un concept abstrait, pas des objets du réel. C'est une classe abstraite.
L'abstraction la plus poussée serait une classe dans laquelle toutes les opérations seraient abstraites et qui ne comporterait aucune donnée. Cela correspond à une interface (de programmation) ou API (Application Programming Interface). Pour une classe, l'API est une forme simplifiée de contrat de service énumérant la liste des opérations publiques proposées, sans pré ou post-condition, ni invariant (voir programmation par contrat).
Le projet compteur illustre la notion d'interface (Compteur) et de classe (CompteurSimple) qui propose les services ainsi spécifiés en implantant l'interface.
Dans le programme principal, le type utilisé pour déclarer l'objet est l'interface. Cette façon de faire correspond à un principe d'abstraction (le principe de substitution de Liskov), l'un des 5 principes de conception d'architectures logicielles plus compréhensibles, flexibles et maintenables : SOLID.
Application de l'abstraction et de l'encapsulation
Le projet heure contient une interface Heure et une classe HeureV1 qui implante l'interface avec une représentation intuitive mais peu efficace (occupation mémoire, complexité algorithmique). Une autre classe HeureV2 implante la même interface avec une représentation plus efficace. La classe EssaiHeure comprend 4 méthodes ayant une Heure en paramètre et le programme principal déclare une variable objet h dont le type est Heure. On fait donc abstraction du choix d'implantation de l'interface …
- dans les sous-programmes
- dans le programme pour la déclaration de l'objet.
La seule instruction qui s'appuie sur un choix d'implantation est l'instanciation. Le principe d'abstraction permet donc de passer d'une implantation à une autre sans pratiquement rien changer au programme. On remarque aussi que l'encapsulation permet des changements de représentation sans incidence sur la manière d'utiliser les objets (puisque l'API est la même et qu'elle est imposée comme seul moyen d'accès aux données - c'est le principe d'encapsulation).
Collections et parcours de collections
Une collection d'objets peut être représentée par une classe. Les opérations réalisables sur une collection étant communément admises (ajout, recherche, suppression …), on les spécifie dans une interface. Cette interface pouvant être implantée de différentes manières (tableau, structures chaînées, fichier…), on peut définir plusieurs classes qui implantent (mot clé implements) l'interface. Un programme utilisant une collection pourra déclarer la variable servant au stockage des objets en utilisant l'interface comme type.
Par contre l'interface et la (ou les) classe(s) ainsi définies doivent pouvoir servir pour différents types d'objets. Il ne faudrait pas devoir définir une classe pour collecter des heures (liste d'alarmes) et en redéfinir une autre pour collecter des dates (liste d'événements) ou des personnes (équipe). On utilise alors des types paramétrés.
Le projet liste illustre ces notions avec l'interface Liste et la classe ListeTableau.
Lorsqu'un traitement doit être effectué sur chacun des éléments d'une collection, il faut parcourir cette collection, ce qui peut être fait de différentes façons (à l'endroit, à l'envers…). Tous les parcours sont décomposables en 4 opérations : initialisation, test de poursuite de parcours, obtention de l'élément courant, passage à l'élément suivant. Cela peut être défini dans une classe. Le principe consistant à définir un concept (parcours par exemple) sous forme d'objet s'appelle la réification. La classe correspondante comprendra 4 méthodes spécifiées dans une interface Parcours. On appelle itérateur une telle classe de parcours.
Le projet liste illustre ces notions avec les interfaces Parcours et Parcourable et les classes ParcoursALEndroit et ParcoursALEnvers, toutes paramétrées.
/!\ Ces classes imbriquées dans la classe ListeTableau sont privées. Elles ne sont donc visibles que dans la classe ListeTableau. Pourtant, elles sont utilisables en dehors même du paquet contenant la classe ListeTableau comme le démontre le programme EssaiListeTableau du paquet application. Cela est rendu possible par l'application du principe de substitution de Liskov.
Exercice pour la prochaine séance
Compléter la classe ListeChainee présente dans l'archive lst2.zip téléchargeable ci-dessus pour réussir tous les tests automatisés de la classe TestListeChainee. Ce travail est à rendre par courrier électronique sous l'intitulé "TP02 NFP121" à l'adresse "philippe.brutus (à) caensup.fr" pour le 21/02/2025 à 8h00 avec en pièce jointe le fichier "ListeChainee.java".
Ce projet contient une implantation de la classe ListeChainee qui réussit tous les tests de la classe TestListeChainee. Il montre aussi que le parcours par indice d'une liste chaînée est inefficace (l'exécution de EssaiListeChainee affiche des points à chaque passage sur un élément de liste).
Parcours de collection avec for-each
Java propose 2 variantes de la structure de contrôle for.
La première utilise une variable de boucle qui sert à la répétition (compteur de boucle) ou à l'accès aux éléments d'une collection (indice de boucle ou index des éléments).
for (int compteur = 0; compteur < 3; compteur++) System.out.print('.');
System.out.println();
String textes[] = {"un", "deux", "trois"};
for (int index = 0; index < textes.length; index++) System.out.println(textes[index]);
La seconde (dite for-each) utilise une variable de boucle qui prend successivement la valeur des éléments de la collection.
String textes[] = {"un", "deux", "trois"};
for (String s : textes) System.out.println(s);
Pour utiliser cette structure de contrôle sur des collections, il faut que celles-ci soient parcourables, c'est-à-dire qu'elles implantent l'interface Iterable de Java. Cela exige que la classe des collections définisse une méthode (iterator) qui renvoie un objet de parcours implantant l'interface Iterator de Java.
Ce projet contient une implantation de la classe ListeChainee qui respecte ces exigences. La classe de test TestListeChainee définit une méthode de test de parcours avec for-each et la classe d'essai EssaiListeChainee met en œuvre un parcours avec for-each.
Pour ne pas devoir réécrire nos classes de parcours, nous avons utilisé une autre solution : l'adaptateur.
Adapter une classe existante pour se conformer à une API
Un adaptateur permet d'utiliser des objets d'une classe présentant des méthodes différentes de celles qu'on voudrait avoir tout en fournissant le service voulu. C'est une classe intermédiaire qui se charge de l'adaptation des API. Cela peut être simple (changement de nom ou changement de nom et appel avec paramètres) ou compliqué (changement de nom et appel de plusieurs méthodes de l'objet adapté dans une méthode de l'adaptateur).
Exercice pour la prochaine séance
Modifier la classe ListeTableau présente dans l'archive lst4.zip téléchargeable ci-dessus pour permettre le parcours de ses instances avec la structure de contrôle for-each.
Modifier les classes ListeTableau et ListeChainee ainsi que leurs classes imbriquées de parcours pour que, pendant un parcours, il soit possible de supprimer de la collection l'élément courant en ajoutant la méthode supprime de l'interface Parcours qui sera traduite dans l'adaptateur correspondant en méthode remove.
Ce travail est à rendre par courrier électronique sous l'intitulé "TP03 NFP121" à l'adresse "philippe.brutus (à) caensup.fr" pour le 10/03/2025 à 8h00 avec en pièce jointe les fichiers source java des classes ListeTableau et ListeChainee.
Principe de réification appliqué à un test
Dans ce projet, le programme EssaiParcoursListeChainee affiche tous les éléments d'une liste, puis seulement ceux qui vérifient un critère particulier.
Pour éviter de dupliquer du code en modifiant le test effectué sur chaque élément de la liste (principe DRY), on implante le test comme méthode d'une classe de test qui implante une interface très simple (Test.java) concrétisant ce qu'est un test, c'est la réification. C'est élégant et efficace car la méthode d'affichage n'a pas à être réécrite pour fonctionner avec n'importe quel critère de sélection des éléments de la liste.
L'inconvénient de cette solution est de devoir définir une nouvelle classe pour chaque nouveau critère de sélection (voir les classes TestChaineDeLongueur4 et TestToujoursVrai). On peut l'éviter au moyen d'une classe anonyme mais le code écrit est un peu compliqué. Le recours à des expressions anonymes ou expressions lambda, simplifie considérablement le code nécessaire.
Notion de stratégie
On parle de stratégies lorsqu'il est possible de réaliser une opération sur un même objet de différentes manières. On peut par exemple afficher tous les éléments d'une collection horizontalement - les uns à la suite des autres, ou verticalement - les uns en dessous des autres. On peut aussi trier une collection en utilisant différentes méthodes de tri (tri bulle, tri sélection, tri fusion, tri rapide …), ou encore chercher dans une liste chaînée un élément désigné par son indice en commençant le parcours par le début de la liste (indice < moitié du nombre d'éléments) ou en commençant le parcours par la fin de la liste (indice >= moitié du nombre d'éléments).
Pour cela, on définit les différentes méthodes de traitement sous la forme d'une méthode execute() dans différentes classes qui implantent chacune l'interface Strategie, qui spécifie ces classes de traitement.
L'objet sur lequel on applique le traitement peut être passé comme argument du constructeur de la classe implantant la stratégie.
Dans ce projet, on a défini une interface Triable pour spécifier des collections sur lesquelles on peut appliquer un tri des éléments et une interface ListeTriable qu'implantent les classes ListeTableauTriable et ListeChaineeTriable.
/!\ Les tris s'appuient sur des opérations de base : la comparaison de 2 éléments et l'échange de 2 éléments. La première des deux opérations s'applique à des éléments dont la classe définit une méthode de comparaison. C'est pourquoi le type en paramètre de l'interface ListeTriable est contraint par l'implantation de l'interface Comparable. C'est pour cette raison que l'interface Heure a été déclarée Comparable et qu'une méthode compareTo a été définie dans les classes HeureV1 et HeureV2.
Exercice pour la prochaine séance
Les classes ListeTableauTriable et ListeChaineeTriable définissent une méthode trie() qui implante la méthode de tri bulle.
Définir sous forme de stratégie (classe implantant l'interface Strategie) le tri bulle (déjà implanté mais pas en stratégie) et le tri sélection, dans les classes ListeTableauTriable et ListeChaineeTriable et modifier la méthode trie() pour que la stratégie appliquée soit le tri bulle quand le désordre est petit et soit le tri sélection quand le désordre est grand (en utilisant la méthode estEnPetitDesordre()).
Définir sous forme de stratégie dans la classe ListeChainee, la recherche d'un élément par son indice en commençant par le début et la recherche d'un élément par son indice en commençant par la fin et modifier les méthodes element(int index) et retire(int index) pour qu'elles appliquent l'une ou l'autre des stratégies en fonction de la valeur du paramètre index et du nombre d'éléments de la liste.
Ce travail est à rendre par courrier électronique sous l'intitulé "TP04 NFP121" à l'adresse "philippe.brutus (à) caensup.fr" pour le 12/03/2025 à 14h00 avec en pièce jointe les fichiers source java des classes concernées.
Dans ce projet, les classes ListeTableauTriable et ListeChaineeTriable contiennent la définition de classes imbriquées TriBulle et TriSelection qui implantent chacune une des deux statégies de tri… et la classe ListeChainee contient la définition de classes imbriquées RechercheDuDebut et RechercheDeLaFin utilisées dans les méthodes retire(int) et element(int).
Dans ce projet, les classes imbriquées RechercheDuDebut et RechercheDeLaFin, définies dans la classe ListeChainee, héritent d'une classe spécifique de statégie (Recherche) pour simplifier le code des méthodes retire(int) et element(int) en respectant le principe de Liskov.
Interface utilisateur graphique
Les utilisateurs d'applications informatiques, que ce soit sur ordinateur, sur tablette numérique ou sur téléphone mobile à écran tactile, interagissent avec les applications au moyen d'une interface graphique (GUI pour Graphical User Interface) et pas avec une interface en ligne de commande (CLI pour Command Line Interface).
Une application avec interface en ligne de commande sollicite l'utilisateur en lui affichant des questions et en attendant sa réponse. L'exécution du programme est suspendue pendant que l'utilisateur compose sa réponse au clavier. Ce n'est que lorsqu'il valide sa réponse (en frappant la touche Entrée) que l'exécution du programme reprend. La logique d'utilisation est imposée par le programme.
Une application avec une interface graphique réagit aux sollicitations de l'utilisateur en les traitant comme des événements. On parle de programmation événementielle pour caractériser le style de programmation qui permet de réagir à des sollicitations très différentes (au clavier, à la souris) et qui peuvent survenir (ou non) à un rythme très variable.
En java, les bibliothèques AWT (Abstract Window Toolkit) et Swing proposent des classes prédéfinies pour construire des programmes avec interface graphique. On trouve par exemple dans la bibliothèque AWT…
- les classes d'événements Clavier (KeyEvent) et Souris (MouseEvent)
- mais aussi la classe des événements sur des fenêtres graphiques (WindowEvent)
et dans la bibliothèque Swing la classe des Fenêtres graphiques (JFrame).
Programmation événementielle
Une application avec interface graphique doit, pour réagir aux sollicitations de l'utilisateur, définir des méthodes de traitement d'événements qui sont spécifiées dans des interfaces dédiées (KeyListener pour les événements clavier et MouseListener et MouseMotionListener pour les événements souris). Elle doit aussi recenser le ou les objets qui vont exécuter ces méthodes (instances de classes qui implantent ces interfaces) auprès de la source d'événements (une fenêtre par exemple).
Remarque : La définition de deux interfaces MouseListener et MouseMotionListener pour gérer les sollicitations à la souris (MouseEvent) est justifiée parce que beaucoup d'applications utilisent la souris (clic, double-clic, entrée ou sortie du pointeur dans ou d'une zone de l'écran…) sans utiliser d'autres sollicitations liées au mouvement de la souris (comme le glisser-déposer). C'est une application du principe I
de SOLID (interface segregation).
Ce projet définit une application affichant une fenêtre dans laquelle l'utilisateur peut dessiner à la souris tout en changeant l'épaisseur (caractères 1 à 9) et la couleur (caractère R pour rouge, V pour vert, B pour bleu, J pour jaune, N pour noir) de dessin. Cela illustre la programmation événementielle et la possibilité pour l'utilisateur de solliciter l'application en même temps au clavier et à la souris (enchevêtrement des événements).
Si l'on devait décliner cette application pour une utilisation à la souris sur ordinateur ou au(x) doigt(s) sur tablette numérique, il faudrait réécrire le code de la fenêtre de dessin. En effet les événements souris seraient remplacés dans l'une des déclinaisons par des événements d'écran tactile. Cette variante évite la réécriture du code de la fenêtre en séparant vue et contrôleur.
Cette version ajoute la possibilité de dessiner des étoiles (en plus des traces) en choisissant un "outil de dessin" (T pour trace, E pour étoile). La solution retenue est une forme de stratégie puisqu'il s'agit d'effectuer un traitement différent pour le même événement (mouseDragged).
Architecture MVC
Dans toutes les versions précédentes, on peut diminuer les dimensions de la fenêtre pour masquer une partie du dessin. Mais lorsqu'on augmente les dimensions de la fenêtre pour voir la totalité du dessin, il ne réapparaît pas… parce qu'il n'est pas mémorisé, donc pas réaffichable. Cette version ajoute des classes dans un paquet modele pour mémoriser le dessin. Elle met en œuvre un principe de séparation de l'interface graphique et du noyau fonctionnel d'une application : l'architecture Modèle-Vue-Contrôleur… mais sans respecter le principe d'indépendance du modèle à la vue. En effet, les méthodes afficheDans(Graphics) des figures prennent en paramètre un élément de la vue.
Comme le même modèle doit pouvoir servir dans des applications aux interfaces utilisateur très différentes, le modèle ne doit pas connaître ni utiliser des éléments de la vue. Cette version réalise l'affichage du dessin dans la classe FenetreDeDessin (méthodes afficheDans et paint) mais doit tester le type des figures alors qu'en programmation par objet, on ne devrait pas tester le type des objets (par application du polymorphisme).
Appel à un ami
Le modèle ne peut pas dépendre de la vue (principe de séparation) et la vue doit traiter (ici afficher) correctement le modèle, c'est-à-dire sans tester le type des objets du modèle. Dans ces conditions, où peut-on définir les méthode d'affichage du modèle ? Dans la vue puisque le modèle doit être indépendant de la vue, mais pas dans la classe FenetreDeDessin, puisque cela oblige à tester le type des objets du modèle. Seule possibilité, dans un classe du paquet vue qui n'est pas la classe FenetreDeDessin. Une classe qui connaît la vue et le modèle… un ami commun.
De manière très générale, il s'agit ici de réaliser une opération (l'affichage dans notre exemple) sur des objets de types différents (traces et étoiles dans notre exemple) en définissant les méthodes correspondantes en dehors des classes concernées (Dessin, Trace et Etoile dans notre exemple). C'est un peu comme quand on nous demande quelque chose qu'on ne sait pas, que celui qui demande ne peut le faire, ni nous non plus puisqu'on ne sait pas… On fait appel à un ami (joker dans le jeu "Qui veut gagner des millions"). Ou encore quand on nous dit de nous soigner parce qu'on se plaint de douleurs alors que nous ne sommes pas médecin. On ne le fait pas nous-même (la méthode n'est pas définie dans la classe de l'objet qui doit réaliser une opération) mais on le fait faire par un tiers (de confiance), le médecin ou le chirurgien. Ce tiers peut réaliser l'opération sur des objets de classes différentes (un vétérinaire peut opérer un chat, un chien, un serpent…).
On définit une interface Operable qui spécifie une seule opération realise(Operation)
. Cette interface doit être implantée par toutes les classes pour lesquelles on souhaite que l'opération les concernant soit définie en dehors d'elle. On définit une interface Operation qui spécifie l'opération pour chacune des classes concernée. On définit une classe pour l'opération en question, qui implante l'interface Operation et définit une méthode pour chaque type d'objet concerné. C'est ce qui est fait dans cette version dont on examinera les interfaces Operable et Operation du paquet modele et la classe Affichage du paquet vue qui définit la méthode d'affichage pour les 3 classes concernées (Dessin, Trace, Etoile). Dans les classes Trace et Etoile du paquet vue, on a défini une méthode realise(Operation
qui permet la réalisation de l'opération (Affichage ou autre) très simplement, puisque le traitement est délégué à l'opération (ami ou médecin ou chirurgien) qui va agir sur l'objet (op.opereSur(this)
).
Et les différentes couleurs et épaisseurs ?
Le modèle est simplifié dans les versions précédentes. En pratique, dans un logiciel de dessin, l'épaisseur et la couleur s'appliquent à la totalité d'une figure (segment, rectangle, ellipse, trace…). Notre exemple, pour illustrer le potentiel de la programmation événementielle, permet de changer d'épaisseur ou de couleur plusieurs fois dans une figure, ce qui complique le modèle comme on peut le voir dans cette version.
Bibliothèques pour des interfaces utilisateur graphiques
Depuis 1995, la bibliothèque AWT permet de programmer des interfaces utilisateur graphiques (IUG) en proposant :
- un ensemble de composants d’interface utilisateur
- un modèle robuste de gestion d’événements
- des outils de gestion de graphiques et d’images comprenant des classes pour les formes, couleurs et polices de caractères
- des gestionnaires d’agencement de composants
…
Depuis 1998 avec la sortie de java 2, la bibliothèque Swing offre la possibilité de créer des interfaces indépendantes du système d’exploitation et propose plusieurs choix d'apparence pour chacun des composants standards. Elle s'appuie sur AWT et, en pratique, on utilise AWT pour les objets qui ne s'affichent pas (événements, écouteurs, agenceurs de composants) et Swing pour les objets qui s'affichent à l'écran (fenêtres, menus et leurs éléments, boutons divers, zones de texte, curseurs…).
En 2008, Sun Microsystems à proposé une alternative à AWT+Swing, plus performante et complète (prise en charge des écrans tactiles par exemple), JavaFX. Reprise par Oracle suite au rachat de java, la bibliothèque JavaFX est intégrée au JDK depuis la version 8 en 2014 et jusqu'à la version 11 de 2018, où le projet est dissocié du JDK. C'est alors la communauté OpenJFX qui poursuit son développement. En tant que bibliothèque externe au JDK, elle est un peu moins pratique à mettre en œuvre que Swing. Notre propos étant la programmation avancée et pas le développement d'interfaces graphiques, nos applications graphiques seront développées avec Swing.