Interface et Collection

Manifeste d'un porte conteneur

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-conteneur (container en anglais) est un bateau qui, comme son nom l'indique, transporte des conteneurs d'un port à l'autre. Chaque porte-conteneur 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), un poids (weight), une valeur entière en kg, ainsi qu'une destination (destination) sous forme de chaîne de caractères.
Écrire le type Container de telle façon à ce que le code suivant fonctionne:

  public static void main(String[] args) {
    var container1 = new Container("DSVX 123456 5", 500, "Germany");
    System.out.println(container1.bic());  // DSVX 123456 5
    System.out.println(container1.weight());  // 500
    System.out.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 tel 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.
A l'avenir, les sujets ne le mentionneront plus explicitement.

Un porte conteneur 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, comment définir un Passenger tel 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 tel 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.

Le prix pour un passager est 10.

Le prix pour un conteneur est le poids du conteneur multiplié par 2.

Ajouter une méthode totalPrice à Manifest et faite en sorte que le prix soit calculés correctement.
          System.out.println(manifest2.price()); // 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"));
    System.out.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");
    System.out.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 cid ou un passager et un conteneur ayant le même identifiant.
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é pire 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 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 à checker 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é 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-conteneur, et si un porte-conteneur 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 weightPerDestination 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"));
    System.out.println(manifest7.weightPerDestination());
      // {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.