:: Enseignements :: Master :: M1 :: 2022-2023 :: Java Avancé ::
![[LOGO]](http://igm.univ-mlv.fr/ens/resources/mlv.png) |
Rappel de notions de programmation objet
|
Pour l'ensemble des TDs de cette année, nous allons utiliser l'IDE Eclipse
(pas de problème si vous voulez en utiliser un autre pourvu qu'il sache
compléter automatiquement, lire le pom.xml de Maven et faire du refactoring).
Eclipse s'exécute en tapant dans un terminal :
eclipse-light
Après démarrage, vérifiez que vous utilisez le bon JDK : dans Window > Preferences > Java > Installed JREs et
assurez-vous que la version est bien compatible avec Java 19.
Puis vérifiez que le compilateur a bien été configuré en mode 19 :
dans Window > Preferences > Java > Compiler, le Compiler compliance level doit être à 19.
Chaque exercice vient avec des tests unitaires
JUnit
qui vous permettent de vérifier que votre implantation passe les tests.
Exercice 1 - Maven
Pour ce TP et les TPs suivant, nous allons utiliser Maven comme outil de build (et de gestion de dépendances).
Maven est un programme que l'on configure en écrivant un fichier pom.xml (Project Object Model)
qui décrit des propriétés, les dépendances et le build (la séquence d'actions pour créer le programme).
Le plugin Maven (m2eclipse) doit être installé (c'est le cas sur les machines de TP de la fac),
sinon il faut aller dans Help > Install New Software... Dans la fenêtre d'installation, sélectionner
2022-09 - https://download.eclipse.org/releases/2022-09 (la version courante de votre Eclipse),
puis dans la barre de filtre (filter), taper Maven et sélectionner M2E puis
appuyer sur Finish. Une fois Eclipse redémarré, le plugin pour Maven sera installé.
Comme nous utilisons Eclipse comme IDE, il faut aussi indiquer à Eclipse qu'au lieu d'utiliser ses fichiers de
configuration habituels (le .classpath et le .projet), la configuration est dans le pom.xml.
De plus, par défaut, Eclipse ne met pas à jour sa configuration en utilisant la configuration du pom.xml,
il faut aller dans les Preferences > Maven et cocher l'option
Automatically update Maven projects configuration.
-
Dans un premier temps, créer un projet Maven (attention pas un projet Java, un projet Maven).
Au niveau du premier écran, cocher create simple project car on va tout faire à la main
plutôt que partir d'un modèle existant puis passer à l'écran suivant en indiquant Next.
Eclipse vous demande d'indiquer un groupId fr.uge.ymca , un artefactId ymca et
une version 0.0.1-SNAPSHOT. Le groupId est plus ou moins le package, l'artefactId le nom du projet.
Une version correspond le plus souvent à 3 nombres séparés par des points
(la norme s'appelle semver), le -SNAPSHOT indique que ce n'est pas une
release, une version disponible pour n'importe qui, mais une version de développement.
Puis cliquer sur Finish.
-
Si tout se passe bien, vous devriez avoir un projet avec le pom.xml suivant qui reprend les informations
(on parle de coordonnées) du projet.
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>fr.uge.ymca</groupId>
<artifactId>ymca</artifactId>
<version>0.0.1-SNAPSHOT</version>
</project>
De plus, Eclipse à créé deux répertoires importants, src/main/java qui va contenir le code source
(les fichiers .java) et src/test/java qui va contenir le code des tests (aussi des fichiers .java)
-
À partir de maintenant, on va configurer le projet en ajoutant des informations de configuration
au pom.xml, votre IDE se mettra à jour automatiquement.
Pour commencer, nous allons indiquer que les fichiers textes sont encodés en UTF-8 en ajoutant la propriété
project.build.sourceEncoding.
<project ...>
...
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
</project>
-
Puis on va ajouter des dépendances, les jar dont dépend notre programme. Dans notre cas, on va utiliser
JUnit 5 (son nom de guerre est jupiter) comme bibliothèque pour les tests
<project ...>
...
<dependencies>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.9.0</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>
On peut remarquer que spécifier une dépendance revient à indiquer ses coordonnées (son groupId, son artifactId,
et sa version). Ici, on utilise la version 5.9.0, car c'est la plus récente. Vous pouvez utiliser la complétion
(le Ctrl + Space) de votre IDE pour voir s'il n'y a pas une version plus récente.
Le scope test, indique que la bibliothèque est disponible uniquement pour les tests, donc
pour les fichiers dans le répertoire src/test/java et pas pour les fichiers source
ceux qui sont dans le répertoire src/main/java.
-
Il nous reste enfin à spécifier la partie build, dans notre cas les options de compilation et les
options des tests.
Pour la compilation, nous allons utiliser le plugin maven-compiler-plugin
<project ...>
...
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.10.1</version>
<configuration>
<release>19</release>
<compilerArgs>
<compilerArg>--enable-preview</compilerArg>
</compilerArgs>
</configuration>
</plugin>
</plugins>
</build>
</project>
Au niveau de la configuration, release permet d'indiquer la version de Java, ici la version 19,
et compilerArgs permet d'ajouter des arguments au compilateur. Ici on ajoute que l'on veut
les preview features (cf plus loin dans le TP).
Pour les tests, nous allons utiliser le plugin maven-surefire-plugin
<project ...>
...
<build>
<plugins>
...
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.0.0-M7</version>
<configuration>
<argLine>--enable-preview</argLine>
</configuration>
</plugin>
</plugins>
</build>
</project>
Pour la configuration, on active aussi les preview features avec argLine, car il faut activer
les preview features à la compilation et à l'exécution (ici, à l'exécution des tests).
-
Pour exécuter Maven, on utilise soit la ligne de commande mvn package soit avec un click bouton droit
sur le pom.xml suivi d'un run build avec le goal package.
Rappel: si vous voulez créer une classe, il faut la mettre dans un package (ici fr.uge.ymca)
dans le répertoire src/main/java. Pour les tests, il faut les mettre dans le même package
(donc aussi fr.uge.ymca) mais dans le répertoire src/test/java.
Exercice 2 - YMCA
Le but de cet exercice est de créer un ensemble d'objets permettant de modéliser une maison
YMCA
comme dans la chanson plutôt que comme dans la vraie vie.
Une maison
House accueil des
VillagePeople. Un
VillagePeople possède
un nom (
name) ainsi qu'une sorte (
kind) parmi COP, NATIVE, GI, BIKER, CONSTRUCTION, COWBOY,
ADMIRAL, ATHLETE, GIGOLO et SAILOR.
Pour définir les différentes sortes de
VillagePeople, on va utiliser un
enum défini
dans un fichier
Kind.java comme ceci
public enum Kind {
COP, NATIVE, GI, BIKER, CONSTRUCTION, COWBOY, ADMIRAL, ATHLETE, GIGOLO, SAILOR
}
Toutes les classes,
enum, etc... doivent être définis dans le package
fr.uge.ymca.
-
Écrire le code de VillagePeople tel que l'on puisse créer des VillagePeople avec leur nom
et leur sorte. Par exemple,
var lee = new VillagePeople("Lee", Kind.BIKER);
System.out.println(lee); // Lee (BIKER)
L'affichage d'un VillagePeople affiche son nom, puis sa sorte entre parenthèses comme
dans l'exemple ci-dessus.
Vérifier que les tests JUnit marqués "Q1" passent.
Attention, il y a toujours un problème avec les tests, si ceux-ci passent, on ne sait pas si le code est correct,
car il peut aussi manquer un test qui va montrer que le code n'est pas correct. On sait juste que le code n'est pas bon quand les tests ne marchent pas.
-
On veut maintenant introduire une maison House qui va contenir des VillagePeople.
Une maison possède une méthode add qui permet d'ajouter un VillagePeople dans la maison (note : il est possible d'ajouter plusieurs fois le même).
L'affichage d'une maison doit renvoyer le texte "House with" suivi des noms des VillagePeople
ajoutés à la maison, séparés par une virgule. Dans le cas où une maison est vide, le texte est "Empty House".
var house = new House();
System.out.println(house); // Empty House
var david = new VillagePeople("David", Kind.COWBOY);
var victor = new VillagePeople("Victor", Kind.COP);
house.add(david);
house.add(victor);
System.out.println(house); // House with David, Victor
Vérifier que les tests JUnit marqués "Q2" passent.
Pour l'affichage, vous pouvez utiliser un StringBuilder.
Attention à ce que l'on ne puisse pas ajouter des VillagePeople null !
-
En fait on veut que l'affichage affiche les noms des VillagePeople dans l'ordre alphabétique,
il va donc falloir trier les noms avant de les afficher. On pourrait créer une liste intermédiaire
des noms puis les trier avec un appel à list.sort(null) mais cela commence à faire
beaucoup de code pour un truc assez simple. Heureusement, il y a plus simple, on va utiliser
un Stream pour faire l'affichage.
Dans un premier temps, ré-écrire le code de l'affichage (commenter l'ancien) pour utiliser un Stream
sans se préoccuper du tri et vérifier que les tests de la question précédente passent toujours.
Puis demander au Stream de se trier et vérifier que les tests marqués "Q3" passent.
Note : pour joindre les éléments d'un Stream qui sont des sous-types de CharSequence
on utilise le collector
Collectors.joining().
Note 2 : Lorsque l'on a une modification à effectuer, la démarche qui consiste dans un premier temps
à changer le code en gardant l'ancien mode de fonctionnement puis introduire le nouveau mode de fonctionnement
s'appelle faire du refactoring. On prépare le terrain pour ajouter la feature sans casser le code
existant, puis on ajoute la nouvelle feature.
-
En fait, avoir une maison qui ne peut accepter que des VillagePeople n'est pas une bonne décision en
termes de business, ils ne sont pas assez nombreux. YMCA décide donc qu'en plus des VillagePeople
ses maisons permettent maintenant d'accueillir aussi des Minions, une autre population sans logement.
On souhaite donc ajouter un type Minion (qui possède juste un nom name) et
changer le code de House pour permettre d'ajouter des VillagePeople ou des Minion.
Un Minion affiche son nom suivi entre parenthèse du texte "MINION".
Vérifier que les tests JUnit marqués "Q4" passent.
Note : on souhaite qu'il soit possible d'ajouter dans le futur d'autres personnes que des VillagePeople
et des Minion...
-
On cherche à ajouter une méthode averagePrice à House qui renvoie le prix moyen pour une nuit
sachant que le prix pour une nuit pour un VillagePeople est 100 et le prix pour une nuit pour un
Minion est 1 (il vaut mieux être du bon côté du pistolet à prouts).
Le prix moyen (renvoyé par averagePrice) est la moyenne des prix des VillagePeople et
Minion présent dans la maison.
Écrire la méthode averagePrice en utilisant le polymorphisme (late dispatch) pour trouver le prix
de chaque VillagePeople ou Minion.
Vérifier que les tests JUnit marqués "Q5" passent.
Note : dans le cas où la maison est vide, on renverra NaN (Not A Number).
-
En fait, cette implantation n'est pas satisfaisante car elle ajoute une méthode publique dans VillagePeople
et Minion alors que c'est un détail d'implantation.
Au lieu d'utiliser la POO (programmation orienté objet), on va utiliser la POD (programmation orienté data)
qui consiste à utiliser le pattern matching pour connaître le prix par nuit d'un VillagePeople
ou un Minion.
Modifier votre code pour introduire une méthode privée qui prend en paramètre
un VillagePeople ou un Minion et renvoie son prix par nuit puis utilisez cette méthode pour
calculer le prix moyen par nuit d'une maison.
Vérifier que les tests JUnit marqués "Q6" passent.
Note: le pattern matching, le switch sur des objets est une preview feature qui doit être activé à la compilation
ainsi qu'à l'exécution.
-
L'implantation précédente pose problème : il est possible d'ajouter une autre personne qu'un VillagePeople
ou un Minion, mais celle-ci ne sera pas prise en compte par le pattern matching.
Pour cela, on va interdire qu'une personne soit autre chose qu'un VillagePeople ou un Minion
en scellant le super type commun.
Faite les changements qui s'imposent et vérifier que les tests JUnit marqués "Q7" passent.
Note : il ne devrait pas y avoir de cas default dans votre switch !
-
On veut périodiquement faire un geste commercial pour une maison envers une catégorie/sorte de VillagePeople
en appliquant une réduction de 80% pour tous les VillagePeople ayant la même sorte
(par exemple, pour tous les BIKERs).
Pour cela, on se propose d'ajouter une méthode addDiscount qui prend une sorte en paramètre
et offre un discount pour tous les VillagePeople de cette sorte.
Si l'on appelle deux fois addDiscount avec la même sorte, le discount n'est appliqué qu'une fois.
Implanter la méthode addDiscount et vérifier que les tests JUnit marqués "Q8" passent.
Attention à l'algo, les discounts ne doivent pas être stockés dans une liste, il y a plus efficace !
-
Enfin, on souhaite pouvoir supprimer l'offre commerciale (discount) en ajoutant la méthode
removeDiscount qui supprime le discount si celui-ci a été ajouté précédemment ou plante
s'il n'y a pas de discount pour la sorte prise en paramètre.
Implanter la méthode removeDiscount et vérifier que les tests JUnit marqués "Q9" passent.
-
Optionnellement, faire en sorte que l'on puisse ajouter un discount suivi d'un pourcentage de réduction, c'est à dire
un entier entre 0 et 100, en implantant une méthode addDiscount(kind, percent). Ajouter
également une méthode priceByDiscount qui renvoie une table associative qui a un pourcentage
renvoie la somme des prix par nuit auxquels on a appliqué ce pourcentage (la somme est aussi un entier).
La somme totale doit être la même que la somme de tous les prix par nuit (donc ne m'oubliez pas les minions).
Comme précédemment, les pourcentages ne se cumulent pas si on appelle addDiscount plusieurs fois.
Écrire les méthodes addDiscount(kind, percent) et priceByDiscount et
vérifier que les tests JUnit marqués "Q10" passent.
Attention : pour un pourcentage de 10 (donc 10%), cela veut dire que le prix de la nuit doit être
90% du prix initial (100% moins 10% de discount).
© Université de Marne-la-Vallée