Interface et Collection

Manifeste d'un porte-conteneurs

Cet exercice reprent l'écriture des classes mutables comme Library dans le TP précédent. Le nouveau concept que nous allons explorer est le polymorphisme.

Un porte-conteneurs (container en anglais) est un bateau qui, comme son nom l'indique, transporte des conteneurs d'un port à l'autre. Chaque porte-conteneurs possède un manifeste (manifest), qui est un document papier contenant une liste de l'ensemble des conteneurs qu'il transporte.
Dans ce TP, nous allons modéliser ce document papier.

Vous écrirez toutes les classes de ce TP dans un package nommé fr.uge.manifest. Vous devez tester toutes les méthodes demandées et vous écrirez tous vos tests dans la classe Application du package fr.uge.manifest.application.

Dans un premier temps, on cherche à définir un Container. Un conteneur possède un code BIC (bic), représenté par une chaîne de caractères ; un poids (weight), qui est une valeur entière en kg ; et une destination (destination), elle aussi représentée par une chaîne de caractères.
Écrire le type Container de sorte que le code suivant fonctionne :

  public static void main(String[] args) {
    var container1 = new Container("DSVX 123456 5", 500, "Germany");
    IO.println(container1.bic());  // DSVX 123456 5
    IO.println(container1.weight());  // 500
    IO.println(container1.destination());  // Germany
    ...
    

Rappel : Comme vous le savez maintenant, il ne doit pas être possible de créer un conteneur avec des valeurs invalides : le code BIC doit exister, le poids doit être positif ou nul et la destination doit exister.
Ce doit être un automatisme pour vous et, à partir de maintenant, les sujets ne le mentionneront plus explicitement.

On veut maintenant introduire la notion de Manifest ; un manifeste contient une liste de conteneurs. Pour l'instant, un manifeste définit une méthode add(conteneur) qui permet d'ajouter un conteneur au manifeste.
Il ne doit pas être possible d'ajouter un conteneur null.
Écrire le type Manifest de sorte que le code suivant fonctionne :

  public static void main(String[] args) {
    ...
    var container2 = new Container("MSCU 789012 3", 400, "Italy");
    var container3 = new Container("ONEZ 345678 2", 200, "Austria");
    var manifest1 = new Manifest();
    manifest1.add(container2);
    manifest1.add(container3);
  }
    

De même qu'il ne doit pas être possible de créer un objet représentant un état invalide, les méthodes publiques des objets doivent vérifier que les paramètres sont valides.
À l'avenir, les sujets ne le mentionneront plus explicitement.

Un porte-conteneurs, comme son nom ne l'indique, pas peut aussi transporter des passagers. Un Passenger est défini par un nom (name) et une destination (destination).
Dans un premier temps, explique comment définir un Passenger de sorte que l'on puisse créer un passager. Puis expliquer comment modifier Manifest pour que l'on puisse enregistrer aussi bien des conteneurs que des passagers.
Écrire le code de Passenger et modifier le code de Manifest de sorte que le code ci-dessous fonctionne.

  public static void main(String[] args) {
    ...
    var passenger1 = new Passenger("Nicolas F", "France");
    var container4 = new Container("OOCL 098765 0", 350, "England");
    var manifest2 = new Manifest();
    manifest2.add(passenger1);
    manifest2.add(container4);
      

On souhaite ajouter une méthode totalPrice à Manifest qui calcul coût total pour transporter tous les conteneurs et tous les passagers du bateau, en utilisant les règles suivantes :

Ajouter une méthode totalPrice à Manifest et faite en sorte que le prix soit calculé correctement.
          IO.println(manifest2.totalPrice()); // 710
      

On souhaite maintenant pouvoir afficher un manifeste. Afficher un manifeste revient à afficher chaque conteneur/passager sur une ligne, avec un numéro : 1 pour le premier conteneur/passager, 2 pour le suivant, etc. Chaque ligne est suivie d'un retour à la ligne, y compris après la dernière ligne.
Pour le formatage exact, vous pouvez regarder l'exemple.
Modifier le type Manifest pour que le code suivant ait le comportement attendu :

  public static void main(String[] args) {
    ...
    var manifest3 = new Manifest();
    manifest3.add(new Container("OOCL 098765 0", 350, "England"));
    manifest3.add(new Passenger("Jane D", "US"));
    IO.println(manifest3);
    // 1. OOCL 098765 0 350kg to England
    // 2. Jane D to US
  }
    

Il arrive que l'on soit obligé de décharger tous les conteneurs liés à une destination s'il y a des problèmes d'embargo (quand un dictateur se dit qu'il s'offrirait bien une partie d'un pays voisin par exemple). Dans ce cas, il faut connaitre tous les conteneurs et passagers liés à cette destination au niveau du manifeste.
Pour prendre en compte cela, on introduit une méthode toDestination(destination) qui renvoie une liste de tous les containers/passagers allant à ayant la destination passée en paramètre
Quelle est le type de retour de toDestination(destination) ? Pourquoi ?
Modifier le code pour introduire cette méthode pour que l'exemple ci-dessous fonctionne:

  public static void main(String[] args) {
    ...  
    var manifest4 = new Manifest();
    manifest4.add(new Container("HAPC 543210 3", 450, "Russia"));
    manifest4.add(new Container("BICU 123456 5", 200, "China"));
    manifest4.add(new Container("CMAU 432109 6", 125, "Russia"));
    manifest4.add(new Passenger("Ana K","Russia"));
    var embargoed = manifest4.toDestination("Russia");
    IO.println(embargoed);
    // [HAPC 543210 3 450kg to Russia, CMAU 432109 6 125kg to Russia, Ana K to Russia]
    

On souhaite maintenant détecter que le manifeste est valide, c'est-à-dire qu'il n'existe pas deux passagers ayant le même nom, ou deux conteneurs ayant le même bic, ou encore un passager et un conteneur ayant le même identifiant (c'est-à-dire un passager dont le nom est égal au bic d'un conteneur).
Pour cela, on se propose de créer une méthode checkIsInvalid qui lève une exception IllegalStateException si le manifeste est invalide.

    var manifest5 = new Manifest();
    manifest5.add(new Passenger("James Bond", "UK"));
    manifest5.add(new Passenger("James Bond", "Iceland"));
    manifest5.checkIsInvalid();  // boom !

    var manifest6 = new Manifest();
    manifest6.add(new Container("HLLY 345678 5", 30, "Slovenia"));
    manifest6.add(new Container("HLLY 345678 5", 40, "France"));
    manifest6.checkIsInvalid();  // boom !
    

Quelle est la complexité dans le pire des cas, si l'on implante checkIsInvalid en faisant deux boucles imbriquées sur les passagers ou conteneurs ?
On se propose plutôt d'utiliser l'interface Set, l'implantation HashSet et la valeur de retour de la méthode add(element).
Décrire en français l'algorithme que l'on doit utiliser.
Quelle est la complexité pire des cas ?
Implanter la méthode checkIsInvalid.

En fait, avoir une méthode checkIsInvalid est vraiment un mauvais design. Le bon design est de vérifier que l'on ne peut pas créer un manifeste invalide plutôt que de permettre de créer un manifeste invalide et se poser la question s'il est valide ou non après.
Commenter la méthode checkIsInvalid et modifier la méthode add pour lever l'exception IllegalStateException dès que l'on essaye d'ajouter un passager ou un conteneur qui va rendre le manifeste invalide.
Note : l'approche qui consiste à vérifier que l'on ne peut pas créer un objet invalide, plutôt que de vérifier à postériori qu'un objet est invalide, est référencée en anglais par les 3 mots parse don't validate.

[Revision] Pour les plus balèzes,
On met les conteneurs ayant la même destination au même endroit sur le porte-conteneurs, et si un porte-conteneurs est mal équilibré il a une fâcheuse tendance à se retourner. Donc, pour aider au placement des conteneurs, il doit être possible de fournir un dictionnaire qui, pour chaque destination, indique le poids de l'ensemble des conteneurs liés à cette destination.
Pour cela, écrire une méthode containerWeightPerDestination qui, pour un manifeste donné, renvoie un dictionnaire qui indique le poids des conteneurs pour chaque destination.
Par exemple, avec le code ci-dessous, il y a deux conteneurs qui ont comme destination "Monaco", avec un poids combiné de 100 + 300 = 400, tandis que "Luxembourg" a un seul conteneur de poids 200.

  public static void main(String[] args) {
    ...
    var manifest7 = new Manifest();
    manifest7.add(new Container("BICU 123456 7", 100, "Monaco"));
    manifest7.add(new Container("CXSB 987654 9", 200, "Luxembourg"));
    manifest7.add(new Container("EYRA 321098 6", 50, "Paris"));
    manifest7.add(new Container("DNVN 543210 8", 300, "Monaco"));
    manifest7.add(new Passenger("Dimitri From", "Paris"));
    IO.println(manifest7.containerWeightPerDestination());
      // {Monaco=400, Luxembourg=200, Paris=50}
  }
    

Note : si vous encore plus balèze, il existe une méthode map.merge() dans l'interface Map qui peut simplifier votre implantation.

Comics

Cet exercice est là pour vous servir d'entraînement en vue du TP noté, uniquement si vous avez déjà terminé l'exercice précédent.

On reprend les Library du TP précédent, et on veut cette fois-ci autoriser la bibliothèque à contenir des Comic en plus des Book de la semaine dernière.

Évidemment, il ne faut pas tout modifier en une fois, sinon on court à la catastrophe et aux bugs imprévus ! On procède donc étape par étape, quitte à commenter temporairement les fonctions qui auront cessé de fonctionner et que l'on va réécrire au fur et à mesure.

Reprendre le code des classes Library et Book de l'exercice 3 du TP précédent, et les placer dans un package intitutlé fr.uge.library.
Écrire ensuite un record Comic, qui possèdera les champs suivants :

On vous laisse trouver des types adaptés pour chacun de ces champs.

Faire en sorte d'afficher vos Comic d'une manière qui vous paraîtra satisfaisante.

Permettre à la Library de stocker à la fois des Book et des Comic.
Vérifier que le code suivant fonctionne.

  var library = new Library();

  var b1 = new Book("Lolita", "Nabokov");
  var b2 = new Book("Les fruits du Congo", "Vialatte");
  var b3 = new Book("Murder is easy", "Christie");
  var c1 = new Comic(
      "Batman: Year One",
      "Frank Miller",
      "David Mazzucchelli",
      12
  );

  library.add(b1);
  library.add(c1);
  library.add(b2);
  library.add(b3);

  IO.println(library);
    

Mettre à jour la fonction findByTitle pour qu'elle puisse renvoyer, toujours en temps constant, le Book ou le Comic dont le titre a été donné en argument, si un tel ouvrage est contenu dans la Library ; ou bien null sinon.
Vérifier que le code suivant fonctionne.

      IO.println(library.findByTitle(b1.title()).equals(b1));
      IO.println(library.findByTitle(b2.title()).equals(b2));
      IO.println(library.findByTitle(b3.title()).equals(b3));
      IO.println(library.findByTitle(c1.title()).equals(c1));
      IO.println(library.findByTitle("Murder is ESIEE") == null);
    

Les scénariste et dessinateur d'un Comic sont tous les deux considérés comme des auteurs à part entière du Comic.
Mettre à jour la fonction removeAllBooksFromAuthor en tenant compte de cette convention.
Créer un test adéquat permettant de vérifier que votre fonction est correcte.

Écrire une méthode costOfComics qui renvoie la somme des coûts de tous les Comic contenu dans une Library.