TP - Refactoring de traitement de graphe
Introduction
Dans ce TP, l'idée est de se mettre en situation où nous devons industrialiser un prototype. Nous allons réfactorer une application qui charge un graphe et calcule le plus court chemin entre deux sommets.
L'application se présente sous la forme d'une API NodeJS Express écrite en TypeScript. Les objectifs sont les suivants :
- Rendre le code plus robuste
- Optimiser le calcul de plus court chemin
- Rendre le calcul de plus court chemin plus générique
- Améliorer les entrées/sorties du programme
Pour cela, nous commencerons par une revue de code en séance et procéderons par étape.
Démarrage
- Forker le projet https://github.com/mborne/tp-graph-ts
- Cloner le fork
- Lire le fichier README pour :- Découvrir l'organisation du code
- Construire le projet et exécuter les tests
- Démarrer et tester l'API
 
Mise en garde
Vous devrez impérativement :
- Livrer un code fonctionnel sur la branche par défaut de votre fork.
- Avoir au moins un commit (voire une branche) par question avec un commentaire d'identifier la question (ex 0.1 - blindage de la construction des arcs)
Pour ce faire, il vous est vivement conseillé de :
- Lancer à chaque étape la construction et les tests automatiques.
- Compléter ces tests automatiques avec un test manuel sur l'API.
0.1 - Blindage de la construction des arcs
Nous remarquons que le modèle ne protège pas contre les erreurs la création des sommets et des arcs. Il est possible de créer par erreur un Edge avec une source ou une target nulle.
Nous procèdons comme suit :
- Ajouter un constructeur Edge(source: Vertex, target: Vertex)
- Passer sourceettargeten privées en les renommant en_sourceet_target
- Ajouter Edge.getSource()etEdge.getTarget()
- Adapter le code et les tests.
0.2 - Ajout de fabriques pour les sommets et les arcs
En ajoutant un constructeur Edge(source: Vertex, target: Vertex), nous avons simplifié la création des arcs et des sommets.
Toutefois, les opérations de création demeurent complexes et il reste la possibilité d'oublier d'ajouter les éléments aux listes de vertices et edges du graphe.
Nous allons donc procéder comme suit :
- Ajouter une fabrique createVertex(coordinate: Coordinate, id: String): VertexdansGraph
- Ajouter une fabrique createEdge(source: Vertex, target: Vertex, id: String): EdgedansGraph
- Adapter le code et les tests pour utiliser ces fabriques.
Ainsi, nous passerons de :
const a = new Vertex();
a.setId("a");
a.setCoordinate(new Coordinate(0.0, 0.0));
graph.getVertices().add(a);
const b = new Vertex();
b.setId("b");
b.setCoordinate(new Coordinate(1.0, 0.0));
graph.getVertices().add(b);
const ab = new Edge(a, b);
ab.setId("ab");
graph.getEdges().add(ab);
à
const a = graph.createVertex(new Coordinate(0.0, 0.0),"a");
const b = graph.createVertex(new Coordinate(1.0, 0.0),"b");
graph.createEdge(a,b,"ab");
0.3 - Géométrie réelle des tronçons
Nous remarquons au niveau de Edge.getGeometry() que la géométrie des tronçons est un segment entre le sommet source et le sommet cible. La géométrie réelle est lue dans BdtopoLoader mais elle n'est pas exploitée lors de la création du graphe.
Nous allons procéder comme suit pour ajouter la géométrie réelle des tronçons de route sur les Edge :
- Ajouter un attribut geometry: LineStringsur la classeEdgeavec un settersetGeometry(geometry: LineString)
- Mettre à jour getGeometry()pour renvoyer l'attributgeometrys'il est défini
- Mettre à jour BdtopoLoaderpour stocker la géométrie sur lesEdgeà l'aide desetGeometry
0.4 - Indexation des arcs entrants et sortants
En lisant attentivement RoutingService, nous remarquons que la méthode graph.getOutEdges(vertex: Vertex) est appelée très fréquemment dans la méthode visit(vertex: Vertex).
Cette approche étant loin d'être optimale, nous allons indexer les arcs sortants et entrants comme suit :
- Ajouter des attributs _inEdges: Edge[]et_outEdges: Edge[]surVertex.
- Remplir automatiquement _inEdgeset_outEdgesdans le constructeurEdge(source,target)
- Adapter Graph.getInEdgesetGraph.getOutEdgespour exploiter_inEdgeset_outEdges
Remarques :
- Nous n'avons pas besoin pour l'algorithme actuel des inEdgesmais nous choisissons de conserver une symétrie dans le modèle.
- Dans l'idéal, il faudrait permettre la modification de _inEdgeset_outEdgesmais les TypeScript n'offre pas de mécanisme de visibilité le permettant à ma connaissance (pas de notion de "friend" et pas de visibilité à l'échelle d'un "package").
0.5 - Création d'un modèle dédié aux noeuds de l'arbre du plus court chemin
Nous constatons que Vertex est porteur de propriétés qui ne correspondent pas à la modélisation d'un réseau routier mais à l'algorithme de calcul du plus court chemin : cost, reachingEdge et visited.
Ceci a un lourd impact sur l'application : Il est en l'état impossible de lancer en parallèle deux calculs de plus court chemin car il y a aurait des conflits en édition sur les propriétés des Vertex pendant l'exécution de l'algorithme.
Nous procédons dans un premier temps comme suit pour refondre RoutingService en limitant les reprises de code à effectuer :
- Créer une classe PathNodedans correspondant à un noeud de l'arbre
- Migrer les attributs cost,reachingEdgeetvisiteddeVertexversPathNode
- Ajouter une propriété nodes: Map<Vertex,PathNode>dansRoutingService
- Mettre à jour initGraphdansRoutingServicepour initialisernodes
- Ajouter une méthode utilitaire getNode(vertex: Vertex): PathNodedansRoutingService
- Mettre à jour le reste du code de la classe RoutingServiceà l'aide degetNode
0.8 - Création d'un modèle dédié à l'arbre du plus court chemin
Pour finaliser la mise en oeuvre d'un modèle dédié à l'arbre de plus court chemin, nous encapsulons nodes: Map<Vertex, PathNode> de RoutingService sous forme d'un arbre de plus court chemin nommé PathTree :
- Créer une classe PathTree
- Migrer les éléments correspondant de RoutingServiceversPathTree- initGraph(origin)devient- PathTree(graph: Graph, origin: Vertex)
- buildPath(vertex)devient- pathTree.getPath(destination: Vertex)
- getNode(vertex)devient- pathTree.getNode(vertex: Vertex)
 
0.9 - Stockage des seuls sommets atteints dans PathTree
Nous remarquons qu'il est inutile de stocker des PathNode pour tous les sommets du graphe. Il suffit d'initialiser la liste des nodes avec l'origine des chemins et de créer les PathNode quand de nouveaux sommets sont atteints.
A l'exception du test pour savoir si la destination est atteinte dans RoutingService, les appels à getNode correspondent à des sommets atteints.
Nous allons donc :
- Adapter PathTree(graph, origin)enPathTree(origin)pour initialisernodesavec la seule origine.
- Ajouter une méthode PathTree.isReached(vertex)pour tester si un sommet est atteint.
- Ajouter une méthode PathTree.getOrCreateNode(vertex)pour récupérer ou créer un noeud.
- Ajouter une méthode pathTree.getReachedVertices(): Vertex[]
- Adapter RoutingServiceen utilisant ces méthodes et en veillant à parcourir les seuls sommets atteints dansfindNextVertex