===== 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 {{:departement_info:personnels:pb:pa3:Compteur0.zip|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//.\\ {{:departement_info:personnels:pb:pa3:cpt0.png?150x0&nolink}} 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 {{:departement_info:personnels:pb:pa3:Compteur1.zip|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.\\ {{:departement_info:personnels:pb:pa3:cpt1.png?150x0&nolink}} Le projet {{:departement_info:personnels:pb:pa3:bonjour1.zip|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).\\ {{:departement_info:personnels:pb:pa3:bonjour1.png?400x0&nolink}} 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 {{:departement_info:personnels:pb:pa3:qualité.pdf|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 {{:departement_info:personnels:pb:pa3:javadoc.pdf|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 {{:departement_info:personnels:pb:pa3:progr_defensive.pdf|programmation défensive}}\\ - la {{:departement_info:personnels:pb:pa3:progr par contrat.pdf|programmation par contrat}}\\ Des {{:departement_info:personnels:pb:pa3:assertion.pdf|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'{{:departement_info:personnels:pb:pa3:exceptions.pdf|exception}} peut aussi être mis en œuvre pour rendre le logiciel robuste. {{:departement_info:personnels:pb:pa3:assert1.zip|exemple 1 d'assertion}} {{:departement_info:personnels:pb:pa3:assert2.zip|exemple 2 d'assertion}} {{:departement_info:personnels:pb:pa3:exception.zip|exemple d'exceptions}} {{:departement_info:personnels:pb:pa3:progr par contrat.zip|exemple de programmation par contrat}} La fiabilité du logiciel peut être vérifiée par des {{:departement_info:personnels:pb:pa3:test.pdf|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). {{:departement_info:personnels:pb:pa3:junit.pdf|JUnit}} est un cadriciel de test automatisé pour les programmes écrits en Java. {{:departement_info:personnels:pb:pa3:junit.zip|exemple de classe de test}} /!\ 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 (//[[https://fr.wikipedia.org/wiki/Mock_(programmation_orient%C3%A9e_objet)|mock objects]]//). Il en existe différentes sortes : bouffon (//dummy//), substitut (//fake//), bouchon (//stub//)... {{:departement_info:personnels:pb:pa3:mock.zip|exemple de doublures}} ==== 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 {{:departement_info:personnels:pb:pa3:testdate.zip|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 {{:departement_info:personnels:pb:pa3:date.zip|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//).\\ {{:departement_info:personnels:pb:pa3:date.png?300x0&nolink}} 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 on 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 {{:departement_info:personnels:pb:pa3:bonjour2.zip|bonjour}} met en œuvre une organisation en paquets des classes.\\ {{:departement_info:personnels:pb:pa3:bonjour2.png?250x0&nolink}} ==== 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...).\\ {{:departement_info:personnels:pb:pa3:stock.png?1000x0&nolink}} 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 {{:departement_info:personnels:pb:pa3:compteur2.zip|compteur}} illustre la notion d'interface (Compteur) et de classe (CompteurSimple) qui propose les services ainsi spécifiés en implantant l'interface.\\ {{:departement_info:personnels:pb:pa3:cpt2.png?130x0&nolink}}\\ 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 [[https://fr.wikipedia.org/wiki/Barbara_Liskov|Liskov]]), l'un des 5 principes de conception d'architectures logicielles plus compréhensibles, flexibles et maintenables : {{https://fr.wikipedia.org/wiki/SOLID_(informatique)|SOLID}}. ==== Application de l'abstraction et de l'encapsulation ==== Le projet {{:departement_info:personnels:pb:pa3:heure.zip|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 {{:departement_info:personnels:pb:pa3:lst1.zip|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 [[https://fr.wikipedia.org/wiki/R%C3%A9ification#Informatique|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 {{:departement_info:personnels:pb:pa3:lst2.zip|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". {{:departement_info:personnels:pb:pa3:lst2.png?800x0&nolink}} Ce {{:departement_info:personnels:pb:pa3:lst3.zip|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 [[https://docs.oracle.com/javase/8/docs/api/java/lang/Iterable.html|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 [[https://docs.oracle.com/javase/8/docs/api/java/util/Iterator.html|Iterator]] de Java. Ce {{:departement_info:personnels:pb:pa3:lst4.zip|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). {{:departement_info:personnels:pb:pa3:adaptateur.zip|exemple d'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 {{:departement_info:personnels:pb:pa3:lst5.zip|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 ([[https://fr.wikipedia.org/wiki/Ne_vous_r%C3%A9p%C3%A9tez_pas|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 {{:departement_info:personnels:pb:pa3:lambda.pdf|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 ([[http://lwh.free.fr/pages/algo/tri/tri_bulle.html|tri bulle]], [[http://lwh.free.fr/pages/algo/tri/tri_selection.html|tri sélection]], [[http://lwh.free.fr/pages/algo/tri/tri_fusion.html|tri fusion]], [[http://lwh.free.fr/pages/algo/tri/tri_rapide.html|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 [[https://fr.wikibooks.org/wiki/Patrons_de_conception/Strat%C3%A9gie|Strategie]], qui spécifie ces classes de traitement. {{:departement_info:personnels:pb:pa3:stratégie.png?200x0&nolink}} L'objet sur lequel on applique le traitement peut être passé comme argument du constructeur de la classe implantant la stratégie. Dans {{:departement_info:personnels:pb:pa3:lst6.zip|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 //[[https://docs.oracle.com/javase/8/docs/api/java/lang/Comparable.html|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 {{:departement_info:personnels:pb:pa3:lst7.zip|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 {{:departement_info:personnels:pb:pa3:lst8.zip|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. En pratique, il peut être préférable d'ajouter un élément à sa place dans une liste plutôt que de trier la liste après un ajout. C'est ce qui est fait dans {{:departement_info:personnels:pb:pa3:lst9.zip|ce projet}}, avec la classe ListeChaineeTriee et sa méthode ajoute, dont l'inconvénient est que le temps d'exécution d'un ajout dépend de la place de l'élément ajouté dans la liste. ==== 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 [[https://fr.wikipedia.org/wiki/Programmation_%C3%A9v%C3%A9nementielle|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 (//[[https://docs.oracle.com/javase/8/docs/api/java/awt/event/KeyEvent.html|KeyEvent]]//) et Souris (//[[https://docs.oracle.com/javase/8/docs/api/java/awt/event/MouseEvent.html|MouseEvent]]//)\\ - mais aussi la classe des événements sur des fenêtres graphiques (//[[https://docs.oracle.com/javase/8/docs/api/java/awt/event/WindowEvent.html|WindowEvent]]//)\\ et dans la bibliothèque Swing la classe des Fenêtres graphiques (//[[https://docs.oracle.com/javase/8/docs/api/javax/swing/JFrame.html|JFrame]]//). === Programmation événementielle === Une {{:departement_info:personnels:pb:pa3:appliGraphique.pdf|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 (//[[https://docs.oracle.com/javase/8/docs/api/java/awt/event/KeyListener.html|KeyListener]]// pour les événements clavier et //[[https://docs.oracle.com/javase/8/docs/api/java/awt/event/MouseListener.html|MouseListener]]// et //[[https://docs.oracle.com/javase/8/docs/api/java/awt/event/MouseMotionListener.html|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 (//[[https://fr.wikipedia.org/wiki/Principe_de_s%C3%A9gr%C3%A9gation_des_interfaces|interface segregation]]//). {{:departement_info:personnels:pb:pa3:appliDeDessin1.zip|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. {{:departement_info:personnels:pb:pa3:appliDeDessin2.zip|Cette variante}} évite la réécriture du code de la fenêtre en séparant vue et contrôleur. {{:departement_info:personnels:pb:pa3:appliDeDessin3.zip|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. {{:departement_info:personnels:pb:pa3:appliDeDessin4.zip|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 {{:departement_info:personnels:pb:pa3:MVC.pdf|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. {{:departement_info:personnels:pb:pa3:appliDeDessin5.zip|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 une 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 ([[https://fr.wikipedia.org/wiki/Qui_veut_gagner_des_millions_%3F|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 hamster...). On l'appelle un visiteur. === Visiteur === 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'elles. 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é. {{:departement_info:personnels:pb:pa3:visiteur.png?600x0&nolink}} C'est ce qui est fait dans {{:departement_info:personnels:pb:pa3:appliDeDessin6.zip|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 {{:departement_info:personnels:pb:pa3:appliDeDessin7.zip|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 {{:departement_info:personnels:pb:pa3:awt.pdf|AWT}} pour les objets qui ne s'affichent pas (événements, écouteurs, agenceurs de composants) et {{:departement_info:personnels:pb:pa3:swing.pdf|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. ---- {{:departement_info:personnels:pb:pa3:lst10.zip|Ce projet}} implante une application graphique permettant l'ajout, la modification et la suppression d'alarmes. Elle applique la séparation modèle-vue-contrôleur avec un contrôleur qui implante les différentes fonctions proposées dans l'interface graphique et réalise un traitement en parallèle pour faire sonner une alarme quand l'heure est venue, en héritant de la classe //{{:departement_info:personnels:pb:pa3:threads.pdf|Thread}}// et en redéfinissant sa méthode //run//. Cette application affiche une boîte de dialogue donnant des informations sur sa version lorsque l'utilisateur choisit l'option "A propos" dans le menu "Alarmes". Le dialogue n'est pas bloquant et l'utilisateur peut toujours interagir avec la fenêtre de l'application sans avoir fermé le dialogue "A propos". Il peut aussi choisir à nouveau "A propos" dans le menu "Alarmes" et faire apparaître un nouvel exemplaire de la même fenêtre. Pour l'éviter, on modifie la classe DialoqueAPropos pour qu'il ne soit pas possible de l'instancier plus d'une fois. Le dialogue qui s'affiche lorsqu'une alarme sonne n'a pas besoin de rester affiché longtemps après la sonnerie de l'alarme (après l'heure, ce n'est plus l'heure). On fait donc de même avec la classe DialogueAlarme. Cela permettra de plus de ne charger qu'une seule fois le son de l'alarme. ==== Singleton ==== Une classe ayant au plus une instance est appelée un singleton. Pour empêcher le programmeur utilisant une telle classe de l'instancier plus d'une fois, on interdit l'utilisation du constructeur... en le rendant privé ! Pour qu'il soit néanmoins possible d'instancier la classe, on propose une méthode de classe qui appelle le constructeur s'il n'y a pas encore d'instance de la classe ou renvoie l'instance déjà créée dans le cas contraire. Il faut pour cela mémoriser l'instance unique de la classe dans une variable de classe. {{:departement_info:personnels:pb:pa3:singleton.png?150x0&nolink}} ---- {{:departement_info:personnels:pb:pa3:lst11.zip|Cette version}} implante DialogueAPropos et DialogueAlarme comme des singletons. Cela change la manière d'afficher ces dialogues :\\ - new DialogueAPropos().affiche() devient DialogueAPropos.getInstance().affiche()\\ - new DialogueAlarme().affiche(a) devient DialogueAlarme.getInstance().affiche(a)\\ ==== Entrées-sorties ==== Un programme doit pouvoir échanger des données avec son environnement :\\ - lire au clavier\\ - écrire à l'écran\\ - lire dans un fichier\\ - écrire dans un fichier\\ - ... Toutes ces opérations sont vues comme des lectures ou écritures dans des fichiers. C'est le résultat d'une harmonisation des entrées-sorties destinée à faciliter la tâche du programmeur. On distingue communément\\ - les fichiers (de type) texte\\ - les fichiers binaires En Java, la paquet //java.io// fournit (des classes qui proposent) des services...\\ - d'entrées-sorties au moyen de flux de données (//data streams//) - de sérialisation d'objets\\ - d'accès au système de fichiers Les classes //FileInputStream// et //FileOutputStream// permettent de lire ou écrire des octets. Les classes //FileReader// et //FileWriter// permettent de lire ou écrire des caractères. Les classes //BufferedReader// et //BufferedWriter// permettent de lire ou écrire des chaînes de caractères. Les classes //DataInputStream// et //DataOutputStream// permettent de lire ou écrire des données de base (int, float, char, boolean …). Avec ces classes (et d'autres), on peut {{:departement_info:personnels:pb:pa3:es.pdf|enregistrer dans un fichier ou charger depuis un fichier}} des données de tous types. Et comme des erreurs peuvent se produire lors de ces opérations, on les prend en compte via des classes d'exception dédiées. {{:departement_info:personnels:pb:pa3:exemples_es.zip|Cette archive}} contient des exemples de programmes Java réalisant des enregistrements et chargements en fichiers. ---- {{:departement_info:personnels:pb:pa3:lst12.zip|Cette version du projet}} utilise des classes du paquet java.io et certaines de leurs opérations pour enregistrer et charger des listes d'alarmes en mode texte. Malheureusement charger une liste d'alarmes ou quitter l'application font perdre les modifications apportées à la liste et qui n'ont pas été enregistrées. {{:departement_info:personnels:pb:pa3:lst13.zip|Cette version du projet}} utilise des objets intermédiaires (écouteur d'action - pour les éléments de menu ou écouteur de fenêtre - pour la case de fermeture de la fenêtre) pour effectuer (ou pas) le traitement correspondant. Un intermédiaire à qui on confie un traitement que l'on pourrait faire directement et qui le fait ou pas en fonction de critères définis qui constituent des clauses de son contrat est appelé un mandataire. === Mandataire === Un mandataire (//proxy//) s'intercale entre une classe cliente C et une classe fournisseur F (objet du mandat). Au lieu d'utiliser un fournisseur de la classe F, la classe cliente donne procuration pour le faire à un objet de la classe M qui a la même API. Le mandataire relaie (ou pas) tous les appels de méthode à l'objet du mandat de la classe F mais peut effectuer d'autres traitements : mémoriser les résultats pour ne pas les redemander au fournisseur (proxy cache), bloquer certains appels (contrôle d'accès), enregistrer les sollicitations au fournisseur (journalisation)... {{:departement_info:personnels:pb:pa3:mandataire.png?350x0&nolink}} {{:departement_info:personnels:pb:pa3:mandataire.zip|exemple de mandataire de journalisation}} ---- On remarquera que les opérations d'enregistrement et de chargement (EnregistrementTexte, ChargementTexte) s'appliquent à plusieurs classes (Heure - qui est en fait une interface, Alarme et ListeDAlarmes) et sont donc définies comme des visiteurs. Comme il y a différentes manières d'enregistrer ou charger (en mode texte, en binaire, en XML...), on définit plusieurs visiteurs pour implanter ces stratégies sans rien ajouter aux classes Heure, Alarme et ListeDAlarmes (presque) comme dans {{:departement_info:personnels:pb:pa3:lst14.zip|cette version}}. === Architecture en couches === Lire ou écrire un caractère, c'est lire ou écrire 2 octets (avec l'encodage UTF16 des caractères). Lire ou écrire une chaîne de caractères, c'est lire ou écrire plusieurs caractères à la suite et les classes proposant les opérations de plus haut niveau utilisent les opérations définies dans des classes de plus bas niveau... c'est un système en couches. {{:departement_info:personnels:pb:pa3:couches_logicielles.zip|Cet exemple}} illustre ce principe en proposant dans une classe //KeyboardForBoundedIntegers// une méthode //int getIntBetween(int, int)// qui utilise la méthode //int getInt()// définie dans la classe //KeyboardForIntegers//. Cette méthode utilise la méthode //String getString()// définie dans la classe //Keyboard//. La couche la plus basique (//Keyboard//) fournit un service (//getString//) utilisé par la couche au dessus d'elle, qui peut fournir un service de plus haut niveau (//getInt//). Cette couche fournit un service utilisé par la couche au dessus d'elle, qui peut fournir un service de plus haut niveau (//getIntBetween//). La couche de plus haut niveau (//KeyboardForBoundedIntegers//) est un décorateur de la couche inférieure (//KeyboardForIntegers//), qui est elle-même un décorateur pour la couche inférieure (//Keyboard//). ==== Décorateur ==== En plaçant des objets dans des "emballeurs" qui implantent de nouveaux comportements à partir de ceux que proposent les objets "emballés", on peut affecter de nouveaux comportements à des objets, comme dans les systèmes en couches dont les services offerts s'appuient sur les services offerts par les couches inférieures. {{:departement_info:personnels:pb:pa3:décorateur.png?600x0&nolink}} La généralisation utilisant une interface comme couche de base (Decorable par exemple), permet de ...\\ - ajouter des responsabilités à un objet dynamiquement au moment de l'exécution\\ - ajouter des options de manière flexible, sans devoir multiplier les classes par héritage\\ comme dans {{:departement_info:personnels:pb:pa3:options_cumulables.zip|cet exemple}}. ---- Dans la dernière version du projet Alarmes, l'enregistrement et le chargement sont définis comme des opérations en dehors des classes concernées (HeureV1, HeureV2, Alarme et ListeDAlarmes). Cela permet de les définir (et de définir d'autres opérations) sans ajouter ou modifier le code de ces classes. C'est bien le cas pour les classes HeureV1, HeureV2 et l'interface Heure ainsi que pour la classe Alarme. Mais ce n'est pas tout à fait le cas pour la classe ListeDalarmes qui dépend des classes définissant les opérations d'enregistrement et de chargement dans ses méthodes enregistreSous et charge. Pour découpler complètement la classe ListeDAlarmes des classes ChargementTexte, ChargementBinaire, EnregistrementTexte, EnregistrementXML et EnregistrementBinaire, il faudrait que l'instanciation des objets de ces classes soit réalisée en dehors de la classe ListeDAlarmes, par des méthodes de création d'objet qui créent et renvoient un objet dont la classe dépend de paramètre·s d'appel. Une classe qui crée des objets dont la classe dépend de paramètre·s est appelée une fabrique. ==== Fabrique ==== La fabrique est une classe de création d'objets. Elle permet d'instancier des objets dont le type est dérivé d'un type abstrait. La classe exacte de l'objet créé n'est pas connue par l'appelant. Dans {{:departement_info:personnels:pb:pa3:lst15.zip|cette version}} du projet Alarmes, la méthode enregistreSous n'instancie plus l'opération d'enregistrement et la méthode charge n'instancie plus l'opération de chargement. Les 2 méthodes délèguent cette instanciation à une fabrique (GestionDeFichiers) qui propose deux méthodes pour cela : operationPourEnregistrement et operationPourChargement. Chacune d'elle détermine la classe de l'objet à créer en fonction de l'argument d'appel, le nom complet de fichier, qui indique la stratégie par l'extension du nom de fichier. ==== Exercice pour la prochaine séance ==== Compléter {{:departement_info:personnels:pb:pa3:lst16.zip|ce projet}} pour que...\\ - le nom associé à chaque alarme soit lu au chargement et écrit à l'enregistrement des fichiers de version 2 ;\\ - l'on puisse lire les fichiers de version 1. Les tests automatisés définis dans la classe de test TestEnregistrementEtChargementDeListeDAlarmes doivent tous réussir ! Ce travail est à rendre par courrier électronique sous l'intitulé "TP05 NFP121" à l'adresse "philippe.brutus (à) caensup.fr" pour le 05/05/2025 à 8h00 avec en pièce jointe l'archive contenant les seuls fichiers modifiés ou ajoutés (dans leurs paquets respectifs).