:: Enseignements :: ESIPE :: E4INFO :: 2026-2027 :: Java Avancé ::
[LOGO]

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.

Les tests JUnit 5 de cet exercice sont LegoAndBeerTest.java.

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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.

  6. 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.

  7. 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.

  8. 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.