Etape 3 – Arrêts, services et horaires

1 Introduction

Le but de cette étape est d'écrire les classes modélisant les arrêts de transports en commun, les services et les horaires. Ces trois classes font usage de celles écrites lors des deux premières étapes.

1.1 Arrêts

Les véhicules des transports en commun ne s'arrêtent qu'à des endroits bien définis, appelés arrêts, pour laisser monter ou descendre des passagers. Les entreprises de transports en commun distinguent généralement deux notions d'arrêts :

  • les arrêts physiques, qui sont ceux auxquels les véhicules s'arrêtent effectivement,
  • les arrêts commerciaux, qui sont constitués d'un ou plusieurs arrêts physiques regroupés sous un même nom.

Par exemple, l'arrêt du métro m1 nommé EPFL est un arrêt commercial. Il est composé de deux arrêts physiques, un par sens de voyage du métro. Ainsi, une personne désirant aller en direction de Renens doit attendre le métro à un autre endroit—en l'occurrence un autre quai—qu'une personne désirant aller en direction de Lausanne.

Certains arrêts commerciaux peuvent comporter un relativement grand nombre d'arrêts physiques, éloignés de plusieurs dizaines de mètres. Ainsi, à Lausanne, l'arrêt commercial Saint-François comporte cinq arrêts physiques différents, Georgette en comporte six, etc.

Dans le cadre de ce projet, la distinction entre arrêts physiques et commerciaux n'est pas très importante. Pour simplifier les choses, seuls les arrêts commerciaux sont considérés, et leur position est simplement la position moyenne des arrêts physiques les composant.

1.2 Services

Les horaires des transports en commun varient généralement en fonction de la date. Par exemple, il est fréquent que les horaires en semaine ne soient pas les mêmes que le week-end. Afin de prendre cela en compte, nous définissons la notion de service.

Un service est identifié par un nom et possède une plage de validité, qui détermine de quelle date à quelle date il est en vigueur. D'autre part, il possède un ensemble de jours de circulation, qui sont les jours de la semaine durant lesquels il est généralement actif. Finalement, pour pouvoir prendre en compte les cas particuliers comme les jours fériés, un service possède une liste d'exceptions, qui sont soit des dates durant lesquelles il n'est exceptionnellement pas actif, soit des dates durant lesquelles il est exceptionnellement actif.

Par exemple, une entreprise de transports publics changeant ses horaires chaque 1er janvier et ayant une offre différente la semaine et le week-end pourrait définir un service pour les jours de la semaine. Ce service, appelé p.ex. semaine, aurait pour 2014 une plage de validité allant du 1er janvier 2014 au 31 décembre 2014. Ses jours de circulation seraient le lundi, le mardi, le mercredi, le jeudi et le vendredi. Dans sa liste de jours d'inactivité exceptionnelle pourrait p.ex. figurer le 29 mai 2014, ce jour étant le jeudi de l'Ascension, et donc férié. Un tel jour, on peut imaginer que le service du dimanche soit en vigueur. Dès lors, le service du dimanche aurait quant à lui l'exception inverse, c-à-d que le 29 mai 2014 figurerait dans sa liste de jours d'activité exceptionnelle.

1.3 Horaire

Au moyen des notions d'arrêt et de service définies ci-dessus, on peut définir une notion d'horaire, qui n'est rien d'autre qu'un ensemble d'arrêts et un ensemble de services. Il manque toutefois à cette notion d'horaire un élément crucial : les heures de départ et d'arrivée aux différents arrêts associés aux services.

Pour des raisons qui deviendront claires plus tard, cet aspect des horaires sera séparé du reste. La notion d'horaire utilisée ici sera donc celle présentée ci-dessus : un horaire est un ensemble d'arrêts et un ensemble de services.

2 Mise en œuvre Java

La mise en œuvre Java des concepts présentés ci-dessus se fait intégralement dans trois classes du paquetage ch.epfl.isochrone.timetable. Toutefois, comme décrit ci-dessous, deux de ces trois classes possèdent également une classe imbriquée permettant leur construction par étapes.

2.1 Classe Stop

La classe Stop du paquetage ch.epfl.isochrone.timetable, publique et finale, modélise un arrêt nommé et positionné dans l'espace. Il s'agit d'une classe immuable, dont tous les champs sont privés et finaux.

Interface publique

La classe Stop possède un seul constructeur public :

  • Stop(String name, PointWGS84 position), qui construit un nouvel arrêt avec le nom et la position donnés.

Elle possède de plus les méthodes publiques suivantes :

  • String name(), qui retourne le nom de l'arrêt.
  • PointWGS84 position(), qui retourne la position de l'arrêt.
  • String toString(), qui retourne une représentation textuelle de l'arrêt, en l'occurrence son nom. Redéfinit la méthode toString héritée de Object.

2.2 Classe Service

La classe Service du paquetage ch.epfl.isochrone.timetable, publique et finale, modélise un service. Il s'agit d'une classe immuable, dont tous les champs sont privés et finaux.

Interface publique

La classe Service possède un seul constructeur public :

  • Service(String name, Date startingDate, Date endingDate, Set<Date.DayOfWeek> operatingDays, Set<Date> excludedDates, Set<Date> includedDates), qui construit un service avec le nom, la plage de validité, les jours de circulation et les exceptions données. A noter que la plage de validité inclut la date de début et la date de fin. Lève l'exception IllegalArgumentException si la date de fin est antérieure à la date de début, si une des dates listées dans les exceptions est en dehors de la plage de validité du service, ou si l'intersection entre les dates exclues et incluses n'est pas vide.

Elle possède de plus les méthodes publiques suivantes :

  • String name(), qui retourne le nom du service.
  • boolean isOperatingOn(Date date), qui retourne vrai si et seulement si le service est actif le jour donné. Cela n'est le cas que si le jour est dans la plage de validité du service, et qu'il fait partie des jours de circulation mais pas des jours exclus, ou s'il fait partie des jours inclus.
  • String toString(), qui retourne une représentation textuelle du service, en l'occurrence son nom. Redéfinit la méthode toString héritée de Object.

Conseils de programmation

Comme toutes les classes des étapes précédentes, la classe Service est immuable. Jusqu'à présent, garantir l'immuabilité des classes n'était pas bien difficile et consistait généralement à déclarer tous leurs champs finaux.

Avec la classe Service, un nouveau problème se pose : son constructeur reçoit, et doit stocker, des valeurs qui ne sont elle-mêmes pas immuables, en l'occurrence les ensembles operatingDays, excludedDates et includedDates. Or rien ne garantit au constructeur de Service que son appelant ne modifie pas ultérieurement les ensembles qu'il lui a passé, compromettant ainsi l'immuabilité de la classe Service. Cette situation est illustrée par l'exemple suivant, qui modifie l'ensemble included passé au constructeur de Service après la construction :

Set<Date> included = new HashSet<>();
Service s1 = new Service("s1", …, included);

included.add(…);
Service s2 = new Service("s2", …, included);

Le constructeur de Service n'a donc d'autre choix que de faire une copie de chacun des ensembles qu'il reçoit, puis de stocker ces copies dans les champs appropriés. Heureusement, la copie se fait facilement avec les collections Java, car elles sont toutes équipées d'un constructeur « de copie » qui prend en argument une collection et s'initialise avec les éléments qu'elle contient. Par exemple, la classe HashSet possède un tel constructeur utilisable ici.

Attention à ne surtout pas oublier ces copies, car cela produit des problèmes qui sont ensuite très difficile à diagnostiquer !

2.3 Classe Service.Builder

La classe Builder imbriquée statiquement dans la classe Service a pour but de permettre la construction d'un service par étapes. Sa raison d'être est la suivante : la classe Service est immuable, donc une fois construites, ses instances ne peuvent être modifiées. Or le constructeur de Service prend beaucoup de paramètres, et certains d'entre-eux, en particulier les trois ensembles, sont assez complexes.

Pour illustrer le problème que cela pose, imaginons que l'on désire initialiser pas-à-pas les trois ensembles d'un service, puis le créer. Le code pourrait ressembler à ceci :

Set<Date.DayOfWeek> opDays = new HashSet<>();
Set<Date> exclDates = new HashSet<>();
Set<Date> inclDates = new HashSet<>();

// Code pour remplir petit-à-petit les trois ensembles.
// P.ex. pour inclure le 1er janvier 2014, on écrira :
inclDates.add(new Date(1, Month.JANUARY, 2014));

Service s = new Service("s", …, …, opDays, exclDates, inclDates);

Comme ce code l'illustre, chaque fois que l'on désire construire petit-à-petit un service, il faut créer trois variables locales pour ses ensembles, les remplir petit-à-petit et enfin les passer au constructeur. Cela est d'une part fastidieux et d'autre part peu propre, car le fait que les trois ensembles soient en quelque sorte liés et destinés à faire partie d'un même service n'est pas visible.

Une meilleure manière de faire consiste à fournir une classe représentant un service en cours de construction, et c'est le but de la classe Service.Builder. En utilisant cette classe, on peut récrire le code ci-dessus ainsi :

Service.Builder sb = new Service.Builder("s", …, …);

// Code pour remplir petit-à-petit les trois ensembles du bâtisseur.
// P.ex. pour inclure le 1er janvier 2014, on écrira :
sb.addIncludedDate(new Date(1, Month.JANUARY, 2014));

Service s = sb.build();

Une classe telle que Service.Builder, qui représente un objet en cours de construction, s'appelle généralement un bâtisseur (builder en anglais). Les bâtisseurs sont fréquemment utilisés dans les bibliothèques Java, entre autres pour permettre comme ici la construction incrémentale d'objets immuables. Par exemple, la classe String de java.lang étant immuable, on trouve dans la bibliothèque standard la classe StringBuilder, un bâtisseur permettant la construction incrémentale de chaînes de caractères.

A noter qu'il est fréquent que les méthodes qui modifient un bâtisseur retournent le bâtisseur lui-même (c-à-d this) plutôt que rien du tout. Cela permet de chaîner les appels, comme l'exemple suivant l'illustre avec StringBuilder :

int x = …;
StringBuilder b = new StringBuilder();
b.append("x = ").append(x); // appel chaîné

Interface publique

La classe Service.Builder possède un seul constructeur publique :

  • Builder(String name, Date startingDate, Date endingDate), qui construit un nouveau bâtisseur pour un service ayant le nom et la plage de validité donnés. Lève l'exception IllegalArgumentException si la date de fin est antérieure à la date de début.

Elle possède de plus les méthodes suivantes :

  • String name(), qui retourne le nom du service en cours de construction.
  • Builder addOperatingDay(Date.DayOfWeek day), qui ajoute le jour de la semaine donné aux jours de circulation. Retourne this afin de permettre les appels chaînés.
  • Builder addExcludedDate(Date date), qui ajoute la date donnée aux jours exceptionnellement exclus du service. Lève l'exception IllegalArgumentException si la date n'est pas dans la plage de validité du service en construction, ou si elle fait partie des dates incluses. Retourne this afin de permettre les appels chaînés.
  • Builder addIncludedDate(Date date), qui ajoute la date donnée aux jours exceptionnellement inclus au service. Lève l'exception IllegalArgumentException si la date n'est pas dans la plage de validité du service en construction, ou si elle fait partie des dates exclues. Retourne this afin de permettre les appels chaînés.
  • Service build(), qui retourne un nouveau service avec le nom, la plage de validité, les jours de circulation et les exceptions ajoutées jusqu'ici au bâtisseur.

2.4 Classe TimeTable

La classe TimeTable du paquetage ch.epfl.isochrone.timetable, publique et finale, modélise un horaire (selon la définition restreinte donnée plus haut). Il s'agit d'une classe immuable, dont tous les champs sont privés et finaux.

Interface publique

La classe TimeTable possède un seul constructeur public :

  • TimeTable(Set<Stop> stops, Collection<Service> services), qui construit un nouvel horaire ayant les arrêts et les services donnés. Tout comme le constructeur de Service, il doit prendre garde à copier les collections qu'il reçoit pour garantir l'immuabilité des instances.

La classe TimeTable possède de plus les méthodes publiques suivantes :

  • Set<Stop> stops(), qui retourne l'ensemble des arrêts.
  • Set<Service> servicesForDate(Date date), qui retourne l'ensemble des services actifs le jour donné.

Conseils de programmation

La méthode stops pose un nouveau problème lié à l'immuabilité. En effet, elle retourne un ensemble, qui par défaut est modifiables en Java. Un appelant peut donc très bien tenter de le modifier, p.ex. ainsi :

TimeTable t = …;
Set<Stop> stops = t.stops();
stops.clear();

(la méthode clear de l'interface Set supprime tous les éléments d'un ensemble).

Si la méthode stops se contente de retourner l'ensemble des arrêts qu'elle possède en interne, l'appel à clear ci-dessus supprimera la totalité des arrêts de l'horaire, violant ainsi l'immuabilité de la classe ! Il y a deux manières de résoudre ce problème :

  1. La méthode stops peut retourner une nouvelle copie de son ensemble d'arrêts à chaque appel. Ainsi, les modifications éventuelles effectuées par l'appelant n'auront pas d'impact sur la version originale.
  2. La méthode stops peut utiliser la méthode unmodifiableSet de la classe java.util.Collections pour obtenir une version non modifiable de son ensemble d'arrêts, puis retourner cette version. Ainsi, si l'appelant essaie de modifier l'ensemble retourné, l'exception UnsupportedOperationException sera levée.

Vous êtes libres d'utiliser la solution que vous préférez, mais nous conseillons fortement la seconde, car elle évite les copies inutiles et est ainsi nettement moins coûteuse que la première.

2.5 Classe TimeTable.Builder

Tout comme la classe Service, la classe TimeTable possède une classe imbriquée statiquement et nommée Builder facilitant la construction incrémentale d'horaires.

Interface publique

La classe TimeTable.Builder ne possède aucun autre constructeur que le constructeur par défaut. Elle possède de plus les méthodes publiques suivantes :

  • Builder addStop(Stop newStop), qui ajoute un nouvel arrêt à l'horaire en cours de contruction. Retourne this afin de permettre les appels chaînés.
  • Builder addService(Service newService), qui ajoute un nouveau service à l'horaire en cours de construction. Retourne this afin de permettre les appels chaînés.
  • TimeTable build(), qui retourne un nouvel horaire possédant les arrêts et services ajoutés jusqu'ici au bâtisseur.

2.6 Tests

Contrairement à ce que nous avons fait pour les deux premières étapes, nous ne vous donnons pas cette fois de tests complets pour vos classe, c'est à vous de les écrire !

Nous vous fournissons néanmoins des classes de test minimalistes dans l'archive tests-e03.zip, qui ne font rien d'autre que d'appeler chacune des méthodes publiques des classes de cette étape. Le but de ces tests minimalistes est simplement de vous assurer que vous n'avez pas commis de fautes bêtes, p.ex. en nommant de manière incorrecte une entité.

Vous pouvez les ajouter à votre projet comme pour les étapes précédentes, mais n'oubliez surtout pas de les compléter ! Pensez à tester les différents cas délicats liés à l'immuabilité décrits plus haut. Par exemple, écrivez au moins un test qui vérifie que si un service est créé et que l'un des ensembles qui lui est passé est ensuite modifié, cela n'affecte pas le service.

3 Résumé

Pour cette étape, vous devez :

  1. Ecrire les classes Stop, Service, Service.Builder, TimeTable et TimeTable.Builder en fonction des spécifications ci-dessus.
  2. Compléter les classes de test que nous vous fournissons.
  3. Documenter la totalité des entités publiques que vous avez définies.