:: Enseignements :: ESIPE :: E4INFO :: 2026-2027 :: Java Avancé ::
![[LOGO]](http://monge.univ-eiffel.fr/ens/resources/mlv.png) |
Lego & Beer
|
Programmation orientée objet, pattern matching, stream et collectors.
Le but de ce TP est d'implanter une petite application de caisse qui calcule le prix
à payer (le reçu) par un client qui achète, dans un même magasin, des boîtes
de Lego et des bières (Beer).
On va d'abord écrire ce calcul en utilisant le polymorphisme "classique" de la
programmation orientée objet, puis on va réécrire exactement le même calcul
en utilisant le pattern matching sur des types scellés (sealed),
sans toucher à la hiérarchie de classes.
Le but est de comparer les deux approches. Puis, nous irons nous perdre dans les Stream,
les Collectors et les lambdas pour réviser un peu.
Exercice 1 - Maven
Nous allons utiliser Maven avec la configuration, le
pom.xml, suivante
<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.legoandbeer</groupId>
<artifactId>legoandbeer</artifactId>
<version>0.0.1-SNAPSHOT</version>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>6.1.3</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.16.0</version>
<configuration>
<release>27</release>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.6.0</version>
</plugin>
</plugins>
</build>
</project>
Dans Eclipse, créer un projet Maven (pas un projet Java) puis cocher
create simple project (important !)
au niveau du premier écran, puis passer à l'écran suivant en indiquant
Next.
Pour ce TP, le groupId est
fr.uge.legoandbeer, l'artefactId est
legoandbeer et
la version est
0.0.1-SNAPSHOT. Pour finir, cliquer sur
Finish.
Exercice 2 - Polymorphism vs Pattern-Matching
Après moult rebondissements, nous venons enfin d'ouvrir notre magasin "Lego and Beer"
destiné aux motards adeptes de légo et de bière, notre cœur de cible.
Comme le nom du magasin l'indique, un article vendu dans notre magasin est soit une boîte de Lego,
soit une bière (en fait, plusieurs car on a une quantité associée).
Une boîte de Lego est définie par une quantité (quantity, un entier
strictement positif) et un nom (name, non null).
Une bière est définie par une quantité (quantity, un entier strictement
positif) et une sorte de bière (kind) parmi BLONDE, WHITE ou IPA.
-
Dans le package fr.uge.legoandbeer, modéliser avec des classes, interfaces, enums, etc,
les données du domaine (qu'est-ce qu'une bière, qu'est-ce qu'une boîte de Lego, etc).
Chaque constructeur doit valider ses paramètres.
Vérifier que les tests marqués "Q1" passent.
Note : les tests des questions suivantes utilisent sûrement des classes ou
méthodes qui, à ce stade, n'ont pas encore été écrites, donc le code risque
de ne pas compiler. Commentez les autres tests (Q2, Q3, etc.), et vous les
dé-commenterez au fur et à mesure.
-
Chaque article est décrit par une ligne de texte de la forme
"lego,<quantity>,<name>" ou "beer,<quantity>,<kind>"
(par exemple "lego,4,castle" ou "beer,6,ipa"), la sorte de
bière étant insensible à la casse.
Écrire une méthode parseArticle(line) dans Article (si vous n'avez pas nommé l'interface pour les bières et les légos ainsi, faites un "refactor")
qui renvoie l'Article correspondant,
et qui lève une IllegalArgumentException si la ligne ne
contient pas exactement 3 champs séparés par une virgule, si le type d'article
n'est ni "lego" ni "beer", ou si la sorte de bière est inconnue.
Note : pour découper une chaîne de caractères, il existe en Java la méthode
String.split(separator, -1).
Note2 : pour la sorte de bière, il faut être insensible à la casse
indépendamment de la locale par défaut de la machine
(pensez à la Turquie et à son i sans point...).
Vérifier que les tests marqués "Q2" passent.
-
Dans une classe LegoAndBeer, écrire une méthode statique readArticlesFromFile(path)
qui lit un fichier contenant une ligne par article (les lignes vides ou contenants des blancs
(String.isBlank()) devant être ignorées) et
renvoie la liste, non modifiable, des articles correspondants.
Rappel : java.nio.file.Files.lines(path) renvoie un Stream<String>
associé à un descripteur de fichier qu'il faut impérativement fermer.
Vérifier que les tests marqués "Q3" passent.
-
On veut maintenant calculer le prix du reçu (printReceipt) toujours dans la classe
LegoAndBeer.
Le prix d'une boîte de Lego est la quantité multipliée par le nombre de caractères du nom, le tout
multiplié par 20.
Le prix d'une bière est la quantité si la bière est BLONDE, ou trois fois la quantité sinon.
On commence par la solution "orienté objet" classique utilisant le polymorphisme.
-
On souhaite écrire une méthode d'aide (helper method)
printReceipt(articles) qui parcourt la liste des articles avec une boucle
for, calcule la somme, et affiche "sum: " et la somme.
-
On souhaite écrire une méthode main(args) : elle doit vérifier qu'il y a exactement
un argument (le chemin du fichier), sinon afficher l'usage sur stderr
et quitter avec le code 1 ; lire le fichier ; en cas d'erreur afficher le message d'erreur sur
stderr et quitter avec le code 1 ; sinon appeler printReceipt.
Note : N'oubliez pas de gérer correctement les exceptions!
Est-ce que la méthode printReceipt doit-être publique ?
Même question avec la méthode main ?
Écrire les méthodes printReceipt(articles) et main(args).
Vérifier que les tests marqués "Q4" passent.
-
Le problème du polymorphisme, c'est qu'il faut modifier Article (et donc
toutes ses implantations) à chaque fois qu'on veut ajouter un
nouveau calcul. Dans notre cas, le prix n'est pas vraiment une propriété d'un article,
c'est une règle de gestion propre au magasin.
On va préférer réécrire le code en utilisant le pattern matching.
Pour cela, on va garder les types Lego et Beer
mais commenter le code nécessaire au polymorphisme.
A part l'explication ci-dessus, dans quel cas utilise-t-on le polymorphisme
et dans cas, utilise-t-on le pattern matching ?
Réécrire printReceipt en utilisant le pattern matching
et les record patterns.
Vérifier que les tests marqués "Q5" passent et que les tests précédents passent toujours.
-
On veut maintenant afficher, sur une seconde ligne, le nom de la boîte de Lego
la plus chère du reçu. En cas d'égalité de prix,
on garde la première boîte rencontrée dans la liste.
L'affichage doit être "max lego: " suivit du nom de la boîte de Lego.
Il ne doit pas y avoir d'affichage, s'il n'y a pas de boîte de Lego.
Pour cette question, utiliser Collectors.partitioningBy pour séparer les
bières des Lego, puis Stream.max avec un comparateur créé
avec Comparator.comparing* pour obtenir la boîte de Lego
avec le prix unitaire le plus élevé.
Vérifier que les tests marqués "Q6" passent.
-
Le partitioningBy + cast de la question précédente est exactement le
genre de code que le pattern matching est censé nous éviter d'écrire.
Remplacer ce code par une solution utilisant flatMap avec un
switch à base de pattern matching sur Article.
Note: vous devez absolument comprendre comment marche flatMap,
c'est une méthode essentielle qui sert de base à la notion de Gatherer
que nous verrons plus tard en cours.
Astuce : flatMap accepte qu'une fonction renvoie null à la place
d'un stream pour indiquer qu'il n'y a pas d'élément (cela évite d'allouer un stream vide).
Il n'y a pas de nouveaux tests pour cette question : vérifier que les tests
marqués "Q6" passent toujours.
-
On parcourt actuellement la liste des articles deux fois (une fois pour la somme,
une fois pour le Lego avec le prix le plus élevé). Regrouper les deux calculs en un seul
parcours de Stream, en utilisant Collectors.teeing() pour combiner
un Collectors.summingLong() et un Collectors.maxBy() (avec
en intermédiaire un Collector.flatMapping() pour faire l'équivalent du flatMap).
Une fois que vous avez réussi votre implantation, à votre avis,
quelle implantation est la meilleure pour la production entre votre implantation précédente et
votre nouvelle implantation ?
Il n'y a pas de nouveaux tests pour cette question.
© Université de Marne-la-Vallée