Cours de Programmation avancée


TP : agence de location

Consignes à suivre

Consignes pour démarrer le TP

Comme pour le TP 2, on va utiliser git pour la gestion de versions. Il vous faut donc vous reporter aux consignes du TP 2.

Une fois le dépôt téléchargé, vous pouvez compiler et exécuter le code cliquer deux fois sur agency -> application -> run. Cela exécute la méthode

Pour exécuter les tests, il faut passer par l’onglet gradle à droite et cliquer deux fois sur agency -> Tasks -> verification -> test. Pour le moment, cela ne fonctionnera pas car les tests sont en commentaire.

Lien vers le projet gitlab à forker pour le TP : lien projet à forker

L’agence de location

Introduction

Une agence de location (rental agency en anglais) de vehicle (vehicles) offre à ses clients la possibilité de choisir la voiture qui souhaite louer en fonction de différents critères. L’agence propose la location de deux types de vehicles : des motos (motorbikes) et des voitures (cars). Vous allez donc créer deux classes Car et Motorbike qui implémenteront une interface Vehicle.

Tâche 1 : interface Vehicle

Pour cette tâche, vous devez créer une interface Vehicle dans le package agency qui contient les méthodes suivantes :

  • String getBrand() : renvoie la marque du véhicule sous la forme d’une chaîne de caractères.
  • String getModel() : renvoie le modèle du véhicule sous la forme d’une chaîne de caractères.
  • int getProductionYear() : renvoie l’année de fabrication du véhicule.
  • double dailyRentalPrice() : renvoie le prix de la location pour un jour du véhicule.
  • boolean equals(Object o) : teste l’égalité entre le véhicule et l’objet o. Cette méthode devra renvoyer vrai si o correspond au même véhicule que this. On considérera que deux véhicules sont égaux s’ils sont des instances de la même classe et qu’ils ont la même marque, le même modèle et la même année de production. Pour tester si deux objets ont la même classe ont pourra récupérer leur classe avec la méthode getClass() d’Object et l’opérateur == pour tester si deux classes sont égales.
  • String toString() : renvoie une chaîne de caractères qui comprend dans l’ordre et séparé par des espaces :
    • le type de véhicule : Motorbike ou Car
    • la marque (brand) du véhicule,
    • le modèle (model) du véhicule,
    • l’année de fabrication (production year) du véhicule,
    • des détails entre parenthèses sur le véhicule (spécifique à chaque type de véhicule),
    • deux point :
    • le prix par jour du véhicule en euros suivi du caractère €.

Tâche 2 : classe Car

Pour cette tâche, vous aurez besoin de déterminer l’année courante. Pour récupérer la valeur de l’année en cours vous pouvez utiliser la méthode public static int currentYearValue() de la classe TimeProvider du package util qui vous est fourni dans le projet.

Créez une classe Car à l’intérieur du package agency qui implémente l’interface Vehicle. Cette classe possédera les éléments suivants :

  • un attribut brand de type String,
  • un attribut model de type String,
  • un attribut productionYear de type int,
  • un attribut numberOfSeats de type int et qui correspond au nombre de sièges de la voiture,
  • un constructeur Car(String brand, String model, int productionYear, int numberOfSeats) qui devra lever une exception IllegalArgumentException (exception qui existe déjà en Java et donc pas besoin de la définir) dans les deux cas suivants :
    • si l’année donnée en argument est inférieure strictement à 1900 ou supérieur strictement à l’année en cours. Dans ce cas le message de l’exception devra contenir l’année de production donnée en argument du constructeur.
    • si le nombre de sièges est inférieur strictement à 1. Dans ce cas le message de l’exception devra contenir le nombre de siège donné en argument du constructeur.
    Dans les deux cas, le message de type String donné au constructeur de l’exception devra indiquer pourquoi l’exception s’est produite.
  • une méthode String toString() qui devra respecter le contrat défini dans l’interface Vehicle. Les détails d’une voiture à afficher entre parenthèse correspondront à son nombre de sièges. Par exemple, pour une voiture ayant 3 sièges il faudra que la chaîne contiennent (3 seats) alors qu’elle devra contenir (1 seat) si la voiture n’a qu’un siège.
  • une méthode boolean isNew() qui devra retourner true si la voiture a 5 ans ou moins (modèle récent) et faux sinon. Attention votre code devra fonctionner en utilisant l’année en cours : en 2021 les vieux modèles seront ceux datant d’au moins 2016 alors qu’en 2022, les voitures de 2016 seront considérées comme des vieux modèles.
  • une méthode double dailyRentalPrice() qui devra retourner le prix suivant la formule suivante : une voiture (vieux modèle) ayant plus de 5 ans coûtera 20 euros par siège alors qu’une voiture ayant 5 ans ou moins (modèle récent) coûtera 40 euros par siège.

Pour tester votre classe Car, vous pouvez décommenter le code contenu dans la classe de test nommée TestCar qui se trouve dans src -> test -> java puis lancer les tests en cliquant deux fois sur agency-*** -> Tasks -> verification -> test.

Tâche 3 : classes Motorbike et AbstractVehicle

Créez une classe Motorbike à l’intérieur du package agency qui implémente l’interface Vehicle. Cette classe possédera les éléments suivants :

  • un attribut brand de type String,
  • un attribut model de type String,
  • un attribut productionYear de type int,
  • un attribut cylinderCapacity de type int et qui correspond à la cylindrée en centimètre cube de la moto,
  • un constructeur Motorbike(String brand, String model, int productionYear, int cylinderCapacity) qui devra lever une exception IllegalArgumentException (exception qui existe déjà en Java et donc pas besoin de la définir) dans les deux cas suivants :
    • si l’année donnée en argument est inférieure strictement à 1900 ou supérieur strictement à l’année en cours.
    • si la cylindrée est inférieure à 50.
    Dans les deux cas, le message de type String donné au constructeur de l’exception devra indiquer pourquoi l’exception s’est produite.
  • la méthode String toString() qui devra respecter le contrat défini dans l’interface Vehicle. Les détails d’une moto à afficher entre parenthèse avant le prix correspondront à sa cylindrée. Par exemple, pour une moto ayant un moteur de 500cm³ il faudra que la chaîne de caractères contienne (500cm³).
  • la méthode double dailyRentalPrice() devra retourner le prix suivant la formule suivante : une moto coûtera 0,25€ par centimètre cube de cylindrée.

Vous pouvez remarquer que la classe Motorbike a beaucoup de points communs avec la classe Car. Afin d’éviter la duplication de code, nous vous conseillons l’une de faire l’une des deux choses suivantes (et seulement une, pas les deux) :

  • créer une classe abstraite AbstractVehicle qui contiendra le code en commun des deux classes et qui sera étendue par les deux classes Car et Motorbike.
  • créer une interface VehicleTypeSpecifics implémentée par deux classes : CarSpecifics et MotorbikeSpecifics ainsi qu’une classe ConcreteVehicle implémentant Vehicle et qui utilisera la composition avec VehicleTypeSpecifics pour coder le comportement spécifique aux voitures et aux motos.

Tâche 4 : RentalAgency et UnknownVehicleException

Pour cette tâche, vous devez créer une classe RentalAgency à l’intérieur du package agency qui contiendra les éléments suivants (pour le moment) :

  • un attribut List<Vehicle> vehicles qui est la liste des véhicules de l’agence,
  • un constructeur RentalAgency() qui construit une agence sans véhicules,
  • un constructeur RentalAgency(List<Vehicle> vehicles) qui construit une agence avec les véhicules contenus dans la liste spécifiée en argument,
  • une méthode boolean add(Vehicle vehicle) qui ajoute un véhicule à l’agence si celui-ci n’est pas déjà un véhicule de l’agence et qui ne fait rien sinon, cette méthode renvoie true si elle réussi à ajouter un véhicule et false sinon,
  • une méthode void remove(Vehicle vehicle) qui enlève un véhicule à l’agence. Si le véhicule n’est pas un véhicule disponible à l’agence, une exception de type UnknownVehicleException devra être levée (exception définie ci-dessous),
  • une méthode boolean contains(Vehicle vehicle) qui teste si un véhicule est dans l’agence ou pas, c’est-à-dire qui renvoie true si le véhicule est dans l’agence et false sinon.
  • une méthode List<Vehicle> getVehicles() qui renvoie la liste des véhicules de l’agence.

Vous devez aussi créer une classe UnknownVehicleException qui étend RuntimeException et qui correspond à l’exception levée lorsqu’on essaie de supprimer un véhicule n’étant pas dans l’agence. Cette classe contiendra :

  • un attribut Vehicle vehicle correspondant au véhicule qu’on a essayé d’enlever.
  • un constructeur UnknownVehicleException(Vehicle vehicle) évident,
  • la redéfinition de la méthode String getMessage() : cette méthode qui renverra une chaîne de caractère indiquant que le véhicule n’existe pas dans l’agence. Cette chaîne de caractères devra contenir la représentation sous forme de chaîne de caractères du véhicule (obtenu via un appel à toString()).

Pour tester votre classe RentalAgency, vous pouvez décommenter le code correspondant dans la classe de test nommée TestRentalAgency.

Tâche 5 : critères et filtres

On souhaite pouvoir sélectionner parmi les véhicules à louer toutes les véhicules satisfaisant un critère (criterion) donné. Un critère peut être satisfait ou pas par un véhicule, cela signifie que le critère permet de filtrer (ou sélectionner) des véhicules ayant certaines propriétés. Ce concept existe déjà en java (depuis Java 1.8) sous la forme d’une interface générique Predicate<T> qui correspond à l’interface suivante :

/**
 * Represents a predicate (boolean-valued function) of one argument.
 *
 * @param <T> the type of the input to the predicate
 *
 */
public interface Predicate<T> {

    /**
     * Evaluates this predicate on the given argument.
     *
     * @param t the input argument
     * @return {@code true} if the input argument matches the predicate,
     * otherwise {@code false}
     */
    boolean test(T t);
}

Cette interface permet donc de définir un critère (on dit aussi un prédicat) sur des objets d’un type T quelconque. On peut tester le critère avec l’unique méthode abstraite test de l’interface.

Le premier critère qu’on souhaite définir va nous permettre de filtrer les véhicules ayant une marque spécifique.

Écrire une classe BrandCriterion implémentant l’interface Predicate<Vehicle>.

Cette classe contiendra :

  • un attribut String brand correspondant à la marque des véhicules qu’on souhaite sélectionner,
  • un constructeur BrandCriterion(String brand) évident,
  • une méthode boolean test(Vehicle vehicle) qui renvoie true si le véhicule a une marque égale à l’attribut brand et false sinon.

Écrivez une classe MaxPriceCriterion implémentant l’interface Predicate<Vehicle> qui permet de sélectionner les véhicules ayant un prix de location inférieur ou égal à un montant que l’on passera en paramètre du constructeur de la classe.

Ajoutez deux méthodes select et printSelectedVehicles dans la classe RentalAgency qui remplissent les contrats suivants :

  /**
   * Returns the list of vehicles of this agency that satisfy the specified criterion
   * The returned vehicles are then << filtered >> by the criterion.
   *
   * @param criterion the criterion that the selected cars must satisfy
   * @return the list of cars of this agency that satisfy the given criterion
   */
  public List<Vehicle> select(Predicate<Vehicle> criterion)

  /**
   * Prints the vehicles (one by line) of this agency that satisfy the specified criterion
   *
   * @param criterion the criterion that the selected cars must satisfy
   */
  public void printSelectedVehicles(Predicate<Vehicle> criterion)

On peut naturellement souhaiter faire des intersections de critères, ce qui revient à appliquer le et logique entre les critères. On obtient alors un nouveau critère qui est satisfait si et seulement si tous les critères qui le composent sont satisfaits.

Écrivez une classe IntersectionCriterion qui permet de définir des critères par intersection de plusieurs critères.

Cette classe devra être générique avec un paramètre de type T et devra implémenter l’interface Predicate<T>. Elle aura en constructeur prenant en paramètre une liste de critères. Sa méthode test retournera vrai si et seulement tous les critères passe le test.

Pour tester la méthode select, vous pouvez décommenter le code correspondant dans la classe de test nommée TestRentalAgency.

Tâche 6 : gestion des locations

On souhaite ajouter à la classe RentalAgency la gestion de locations de véhicules par des clients. Un client ne peut louer qu’un véhicule à la fois.

Classe Client

Écrivez une classe Client qui permet de définir un client.

Un client aura une année de naissance, un nom de famille et un prénom. Il vous faudra générer les getters de cette classe ainsi que les méthodes equals et hashCode avec le menu generate d’IntelliJ.

Gestion de location dans la classe RentalAgency

Pour gérer des locations, vous allez utiliser une table (interface Map<K,V> du package java.util). Cette interface, qui est implémentée (en autre) par la classe HashMap<K,V>, permet d’associer des clés (de type K) à des valeurs (de type V). Elle contient (entre autre) les méthodes suivantes :

  • boolean containsKey(Object key): Returns true if this map contains a mapping for the specified key.
  • V get(Object key): Returns the value to which the specified key is mapped, or null if this map contains no mapping for the key.
  • V put(K key, V value): Associates (maps) the specified value with the specified key in this map.
  • Collection<V> values() : Returns a Collection view of the values contained in this map.

Dans notre cas, les clients seront les clés et les véhicules qu’ils ont loué seront les valeurs. Une association clé/valeur correspondra donc à un client ayant loué un véhicule. Un client n’est donc présent dans cette table que si il est en train de louer un véhicule. Il en donc supprimé dès qu’il rend un véhicule.

Complétez la classe RentalAgency avec les éléments suivants :

  • un attribut Map<Client, Vehicle> rentedVehicles qui contiendra les associations entre clients et véhicules loués.
  • une méthode double rentVehicle(Client client, Vehicle vehicle) throws UnknownVehicleException, IllegalStateException : permet au client client de louer le véhicule vehicle. Le résultat est le prix de location. L’exception UnknownVehicleException est levée si le véhicule n’existe pas dans l’agence et IllegalStateException est levée s’il est déjà loué ou que le client loue déjà un autre véhicule.
  • une méthode boolean aVehicleIsRentedBy(Client client) : renvoie true si et seulement si client est un client qui loue actuellement un véhicule et donc false sinon.
  • une méthode boolean vehicleIsRented(Vehicle v) : renvoie true si et seulement si le véhicule est actuellement loué, false sinon.
  • une méthode void returnVehicle(Client client) : le client client rend le véhicule qu’il a loué. Il ne se passe rien s’il n’avait pas loué de véhicule.
  • une méthode Collection<Vehicle> allRentedVehicles() : renvoie la collection des véhicules de l’agence qui sont actuellement loués.

Pour tester la classe Client, vous pouvez décommenter le code correspondant dans la classe de test nommée TestClient.

Pour tester les nouvelles méthodes de RentalAgency, vous pouvez décommenter le code correspondant dans la classe de test nommée TestRentalAgency.

Tâches optionnelles

  • Ajout de tests : Ajouter les tests manquants comme par exemple des tests pour la classe Motorbike.
  • Ajout de la javadoc : Ajouter une documentation java (de préférence en anglais) aux élément du codes pour lesquels cela vous semble utile.
  • Nouveaux critères : rajoutez des classes supplémentaires pour les critères de sélection comme :
    • MinPriceCriterion qui permet de sélectionner des véhicules coûtant au moins un prix.
    • ModelCriterion qui permet de sélectionner des véhicules ayant correspondant à un modèle.
  • Sauvegarde/chargement depuis des fichiers : Modifiez votre code pour permettre la sauvegarde sous forme de fichier de la liste des véhicules d’une agence ainsi que le chargement depuis un fichier. L’idée serait de stocker les véhicules sous la forme de chaîne caractères avec les représentations des attributs et des types de véhicule séparés par un symbole spécial (par exemple ;) et de mettre un véhicule par ligne du fichier. Vous pourrez utiliser la classe Scanner pour cela.
  • Prise en charge de rabais : on pourrait imaginer des agences qui gèrent des rabais. Pour cela, vous pouvez faire une classe Discount qui contiendrait par exemple des conditions sur le client et le véhicule, et le taux de rabais en pourcentage que l’agence appliquerait lors d’une location.