Agile Software Development
Chapitre 15 / 15 · 21 min de lecture

Le framework ETS : étude de cas d'un framework réutilisable

Comment Martin et Newkirk ont construit, sur quatre ans, le framework réutilisable du système d'examen des architectes : développement concurrent de plusieurs vignettes, Template Method, State et SMC, et des gains de réutilisation spectaculaires.

Ce dernier chapitre, écrit par Robert C. Martin et James Newkirk, relate un projet logiciel réel mené de mars 1993 à la fin de 1997. Le logiciel fut commandé par l'Educational Testing Service (ETS) et développé par les deux auteurs, accompagnés de quelques ingénieurs d'Object Mentor, Inc. (OMI). L'objet du chapitre n'est pas un nouveau pattern mais une leçon de méthode : comment produire un framework réutilisable, à la fois sur le plan technique et sur le plan managérial. La création de ce framework fut décisive pour le succès du projet, et l'histoire de sa fabrication est instructive — y compris dans ses échecs. Aucun projet ne se déroule dans un environnement parfait, et celui-ci ne fit pas exception ; comprendre les choix de conception suppose donc de comprendre aussi le contexte dans lequel ils furent faits. Nous transposons ici en TypeScript idiomatique les abstractions que le livre présente en C++.

Le projet : examiner les architectes

Pour devenir architecte agréé aux États-Unis ou au Canada, il faut réussir un examen. Cet examen fut conçu par l'ETS sous charte du National Council of Architectural Registration Boards (NCARB). Les épreuves graphiques demandent au candidat de produire des solutions dans un environnement de type CAO : dessiner le plan d'étage d'un bâtiment, concevoir une toiture s'ajustant à un bâtiment existant, ou implanter un bâtiment sur une parcelle avec son stationnement, sa voirie et ses cheminements. Autrefois, ces solutions étaient dessinées au crayon, puis évaluées par un collège de jurés — des architectes chevronnés qui décidaient de la réussite ou de l'échec.

En 1989, le NCARB chargea l'ETS d'étudier la faisabilité d'un système automatisé de délivrance et de notation de ces épreuves graphiques. En 1992, la conclusion était positive, et l'approche objet fut jugée appropriée du fait de la volatilité constante des exigences. OMI fut contacté pour aider à la conception. En mars 1993, OMI obtint un premier contrat pour produire une partie de l'examen ; un an plus tard, fort de ce succès, un second contrat pour produire l'essentiel du reste.

La structure retenue par l'ETS était élégante. L'examen graphique se décomposait en quinze problèmes distincts appelés vignettes, chacune éprouvant un domaine de connaissance précis (le plan d'étage, la conception de toiture, etc.). Chaque vignette se subdivisait elle-même en deux sections : la section de délivrance (« delivery »), interface graphique sur laquelle le candidat dessinait sa solution, et la section de notation (« scoring »), qui relisait la solution et l'évaluait. La délivrance avait lieu près du candidat ; les solutions étaient ensuite transmises à un site central pour y être notées. Bien qu'il n'y eût que quinze vignettes, chacune admettait de nombreux scripts : une même vignette de plan d'étage pouvait demander de concevoir une bibliothèque dans un script, une épicerie dans un autre. Les programmes de vignette devaient donc être écrits de façon générique, pilotés par le script. La plateforme était Windows 3.1 (puis W95/NT), et le langage le C++.

Le premier contrat, en mars 1993, portait sur la plus complexe des vignettes : Building Design, le plan d'étage d'un petit bâtiment à deux niveaux. Ce choix suivait la recommandation de Booch : développer d'abord les éléments à plus haut risque, pour maîtriser le risque et calibrer le processus d'estimation de l'équipe.

L'échec d'un premier framework

À ce stade, Martin et Newkirk étaient les seuls développeurs. Leur contrat ne portait que sur Building Design, mais ils souhaitaient déjà bâtir, en parallèle, un framework réutilisable : d'ici 1997, quinze vignettes devaient être opérationnelles, et un socle commun leur paraissait offrir un avantage décisif, tout en garantissant la cohérence et la qualité — on ne voulait pas que des fonctionnalités similaires se comportent subtilement différemment d'une vignette à l'autre.

Les premières versions furent démontrées en septembre 1993 et bien reçues. Mais, comme souvent, voir le programme fonctionner révéla aux utilisateurs que ce qu'ils avaient demandé n'était pas ce qu'ils voulaient. L'approche de l'essai sur le terrain de janvier 1994 fit déferler les changements ; les deux développeurs furent submergés. Fin 1993, Newkirk passa une semaine avec un ingénieur de l'ETS pour lui apprendre à réutiliser le framework C++ de 60 000 lignes alors disponible. Le résultat fut consternant : la seule façon de réutiliser ce framework consistait à copier-coller des morceaux de son code source dans les nouvelles vignettes. Ce n'était évidemment pas une option viable.

Attention

Avec le recul, deux causes expliquent cet échec. D'abord, l'équipe s'était concentrée sur Building Design à l'exclusion de toutes les autres vignettes. Ensuite, des mois d'exigences mouvantes et de pression sur les délais avaient laissé des concepts spécifiques à Building Design s'infiltrer dans le framework. Plus profondément, les auteurs avaient naïvement tenu pour acquis les bénéfices de l'objet : ils croyaient qu'un bon design objet en C++ suffirait à produire un framework réutilisable. Ils se trompaient, et redécouvrirent ce que l'on savait depuis des années : construire un framework réutilisable est difficile.

La nouvelle stratégie : développer le framework concurremment

En mars 1994, le nouveau contrat signé portait sur un framework et dix vignettes supplémentaires (l'ETS produisant les cinq dernières sur la base de ce framework). Deux ingénieurs rejoignirent l'équipe — Bhama Rao et William Mitchell —, et il fallut changer de stratégie. L'échéance était absolue : l'examen entrait en production en 1997, les candidats passant les épreuves en février et la notation tombant en mai.

Une partie des 60 000 lignes initiales fut conservée, mais la majorité fut jetée. Une option fut explicitement rejetée : concevoir et achever le framework de bout en bout avant d'attaquer la moindre vignette — ce que beaucoup appelleraient une approche dirigée par l'architecture. Les auteurs s'y refusèrent, car elle aurait produit de larges pans de code de framework impossibles à tester dans des vignettes réelles. Ils ne se faisaient pas confiance pour anticiper parfaitement les besoins des vignettes : l'architecture devait être vérifiée presque immédiatement par un usage réel. Ils ne voulaient pas deviner.

Astuce

Comme l'a dit Rebecca Wirfs-Brock : « Il faut construire au moins trois applications ou plus contre un framework (puis les jeter) avant d'être raisonnablement sûr d'avoir bâti la bonne architecture pour ce domaine. » (Booch, Object Solutions, p. 275.) Après leur échec, les auteurs partageaient ce sentiment. Ils décidèrent donc de développer le framework concurremment avec plusieurs vignettes neuves. Quatre vignettes furent lancées en parallèle ; à mesure qu'elles avançaient, les portions qui se révélaient similaires étaient refactorisées sous une forme plus générique, puis réinjectées dans toutes les vignettes. Rien n'entrait dans le framework sans avoir été réutilisé avec succès dans au moins quatre vignettes. Des portions de Building Design furent excisées et traitées de la même manière.

Parmi les éléments communs ainsi versés au framework : la structure de l'écran (fenêtres de messages, fenêtres de dessin, palettes de boutons) ; la création, le déplacement, l'ajustement, l'identification et la suppression d'éléments graphiques ; le zoom et le défilement ; le dessin d'éléments simples (lignes, cercles, polylignes) ; le minutage des vignettes et l'abandon automatique ; la sauvegarde et la restauration des fichiers de solution avec reprise sur erreur ; les modèles mathématiques de nombreux éléments géométriques (ligne, rayon, segment, point, boîte, cercle, arc, triangle, polygone, avec leurs méthodes d'intersection, d'aire, de test d'appartenance d'un point) ; et l'évaluation pondérée de chaque critère de notation. En huit mois, le framework atteignit environ 60 000 lignes de C++, soit un peu plus d'une année-homme d'effort direct — mais réutilisée par quatre vignettes.

Les résultats : une réutilisation spectaculaire

Que faire de l'ancien Building Design ? À mesure que le framework grandissait et que les nouvelles vignettes le réutilisaient, l'ancien Building Design faisait de plus en plus figure d'intrus, à maintenir et faire évoluer séparément. Bien qu'il représentât plus d'une année-homme d'effort, les auteurs furent impitoyables : ils le jetèrent intégralement, s'engageant à le réimplémenter plus tard sur la base du framework.

Le revers de cette stratégie fut un temps de développement initial relativement long : les quatre premières vignettes exigèrent près de quatre années-homme. Mais les chiffres de réutilisation finirent par être remarquables. Une fois ces vignettes achevées, le framework totalisait 60 000 lignes, et les programmes de délivrance étaient étonnamment petits : environ 4 000 lignes de code répétitif (identique d'une vignette à l'autre) et, en moyenne, 6 000 lignes de code applicatif spécifique — la plus petite vignette n'en comptant que 500, la plus grande jusqu'à 12 000.

À retenir

En moyenne, près des cinq sixièmes du code de chaque vignette provenait du framework ; un dixième seulement était propre à la vignette. Après les quatre premières, la productivité explosa : sept programmes de délivrance supplémentaires — dont une réécriture de Building Design — furent achevés en dix-huit mois-homme. Building Design, qui avait coûté plus d'une année-homme la première fois, ne demanda que 2,5 mois-homme à réécrire de zéro avec le framework en place : une multiplication de la productivité par près de 6:1.

Tout au long du projet, des versions intermédiaires furent livrées à l'ETS chaque semaine : l'ETS testait, renvoyait une liste de changements, l'équipe les estimait, et ensemble ils décidaient de la semaine de livraison ; les changements difficiles ou peu importants cédaient le pas aux prioritaires. L'ETS gardait ainsi le contrôle du périmètre et du calendrier. Et malgré ce flux intense de modifications, « la conception du logiciel ne s'est pas défaite ». En février 1997, les candidats commencèrent à passer leurs examens avec ces programmes ; en mai 1997, la notation démarra. Le système fonctionne depuis : tout candidat architecte d'Amérique du Nord passe désormais cet examen avec ce logiciel.

La conception du framework de notation

Comment noter une connaissance ? Le livre l'illustre par un exemple fictif simple : un test de calcul élémentaire de cent problèmes, du plus simple (addition, soustraction) au plus complexe (multiplication, division longue). L'objectif n'est pas seulement de donner une note réussite / échec / indéterminé, mais d'énumérer les forces et faiblesses de l'élève. Un élève qui croit invariablement que 7 × 8 = 42 ratera quantité de multiplications et de divisions — mais s'il fait tout le reste correctement, on voudra le savoir, car la remédiation est triviale ; on pourrait même le faire réussir avec un conseil correctif.

On structure donc le test en hiérarchie de domaines de compétence. Les feuilles de cet arbre sont les features (« caractéristiques ») : des unités de connaissance évaluables, à qui l'on attribue une valeur acceptable (A), inacceptable (U) ou indéterminée (I). Chaque feature parcourt l'ensemble des réponses pour produire son score. La feature « retenue » (Carry), par exemple, examine chaque addition fausse et essaie différentes combinaisons d'erreurs de retenue : si l'on peut établir avec une forte probabilité qu'une erreur de retenue a été commise, le score de la feature est ajusté en conséquence.

Pour dériver la note finale, on fusionne les scores en remontant la hiérarchie, à l'aide de poids et de matrices. À chaque jonction, une matrice associe un facteur de pondération au score de chaque feature, puis fournit la cartographie produisant le score de ce niveau. Sous le nœud Addition, par exemple, la matrice pondère les scores de Carry et Properties — Carry comptant double car jugé plus important — additionne les scores pondérés, et le résultat sert d'indice dans la matrice. Certaines cellules de la matrice restent vides : ce sont des combinaisons impossibles au vu des pondérations courantes. Ce schéma de poids et de matrices se répète à chaque niveau jusqu'à la note finale, ce qui permet aux psychométriciens de l'ETS un réglage très précis.

Evaluator et le pattern Template Method

La structure statique du framework de notation se divise en deux : quelques classes spécifiques à écrire pour chaque application de notation, et le reste, commun à toutes. La classe centrale est Evaluator, une classe abstraite qui représente à la fois les feuilles et les nœuds-matrices de l'arbre de notation. Sa méthode evaluate est appelée quand on veut le score d'un nœud ; elle met en œuvre le pattern Template Method pour fournir une procédure standard d'écriture des scores sur un flux de sortie.

type Score = "A" | "I" | "U" | "F" | "X";

abstract class Evaluator {
  private name = "";

  setName(theName: string): void { this.name = theName; }
  getName(): string { return this.name; }

  // Template Method : la trame est fixée ici, le calcul est délégué.
  evaluate(scoreOutput: string[]): Score {
    const score = this.doEval();
    scoreOutput.push(`${this.name} ${score}`);
    return score;
  }

  // Le « trou » de la trame : chaque dérivée le remplit.
  protected abstract doEval(): Score;
}

La méthode evaluate est la partie invariante (la « trame ») ; elle appelle la méthode abstraite doEval, redéfinie dans chaque dérivée pour effectuer le calcul réel et renvoyer le score, qu'evaluate se charge ensuite de journaliser sous une forme standard. Les feuilles de l'arbre sont représentées par des dizaines de dérivées (une par feature), chacune redéfinissant doEval. Les nœuds-matrices, eux, sont représentés par la classe FeatureGroup.

class FeatureGroup extends Evaluator {
  private matrix = new Matrix();
  // L'équivalent du vector<pair<Evaluator, int>> du C++ : un évaluateur et son rang.
  private evaluators: Array<{ evaluator: Evaluator; rank: number }> = [];

  // Ajoute un nœud enfant avec son rang (le multiplicateur de son score).
  addEvaluator(evaluator: Evaluator, rank: number): void {
    this.evaluators.push({ evaluator, rank });
  }

  // Peuple une cellule de la matrice.
  addMatrixElement(i: number, u: number, s: Score): void {
    this.matrix.setScore(i, u, s);
  }

  protected doEval(): Score {
    let sumI = 0;
    let sumU = 0;
    const output: string[] = [];
    for (const { evaluator, rank } of this.evaluators) {
      const s = evaluator.evaluate(output);
      if (s === "I") sumI += rank;
      else if (s === "U") sumU += rank;
    }
    // Les accumulateurs servent d'indices pour extraire le score final.
    return this.matrix.getScore(sumI, sumU);
  }
}

addEvaluator ajoute un enfant en précisant son rang — le multiplicateur appliqué à son score (Carry, deux fois plus lourd que Properties, reçoit un rang de 2). addMatrixElement peuple une cellule de la matrice. Enfin, doEval itère sur les évaluateurs, multiplie chaque score par son rang, accumule séparément les I et les U, puis utilise ces accumulateurs comme indices dans la matrice pour en tirer le score final.

Reste une question : comment l'arbre se construit-il ? Les psychométriciens voulaient pouvoir changer la topologie et les pondérations sans modifier les applications. L'arbre est donc bâti par une classe VignetteScoringApp, propre à chaque application, dont une responsabilité est de produire un dérivé de FeatureDictionary — une table associant des chaînes à des Evaluator. Au démarrage, le framework prend la main, fait créer le bon dictionnaire, puis lit un fichier texte décrivant la topologie et les poids de l'arbre en désignant les features par leurs noms — les mêmes que ceux du dictionnaire. Dans sa forme la plus simple, une application de notation n'est donc qu'un ensemble de features plus une méthode qui construit un FeatureDictionary ; la construction et l'évaluation de l'arbre sont prises en charge par le framework, communes à toutes.

Template Method ou Strategy ? « Écrire la boucle une seule fois »

Une vignette demandait au candidat de tracer le plan d'un bâtiment — pièces, couloirs, portes, fenêtres, ouvertures, escaliers, ascenseurs. Le programme convertissait le dessin en une structure de données de simples porteurs de données (un bâtiment de deux étages, chaque étage contenant des espaces, chaque espace des portails, qui pouvaient être des fenêtres ou des passages humains, eux-mêmes portes ou ouvertures de mur). La notation testait cette solution contre un ensemble de features : tous les espaces requis sont-ils dessinés ? Chaque espace a-t-il un rapport de forme acceptable, une entrée ? Les espaces extérieurs ont-ils des fenêtres ? Peut-on rejoindre chaque pièce par les couloirs ?

Pour des raisons de performance, on ne calculait que les features incluses dans la matrice ; chacune était donc une classe à part, dotée d'une méthode d'évaluation qui parcourait la structure de données pour calculer un score. D'où des dizaines de classes qui parcouraient toutes la même structure : une duplication de code épouvantable. La parade — découverte dès 1993-1994, bien avant de connaître le nom des patterns — fut baptisée « Écrire la boucle une seule fois » : c'était le pattern Template Method.

// La boucle de parcours est écrite UNE seule fois, dans la classe de base.
abstract class SolutionSpaceFeature extends Evaluator {
  constructor(private query: Query<SolutionSpace>) { super(); }

  // La trame : on balaie les espaces solution, on délègue chaque visite.
  protected doEval(): Score {
    const spaces = scoreFilter.getSolutionSpaces();
    for (const space of spaces.select(this.query)) {
      this.newSolutionSpace(space);
    }
    return this.getScore();
  }

  // Les « trous » : remplis par chaque feature concrète.
  protected abstract newSolutionSpace(space: SolutionSpace): void;
  protected abstract getScore(): Score;
}

La méthode doEval boucle sur tous les SolutionSpace, puis appelle la méthode abstraite newSolutionSpace, que les dérivées implémentent pour mesurer chaque espace selon leur propre critère (les espaces requis sont-ils présents, ont-ils la bonne aire, le bon rapport de forme, les ascenseurs s'empilent-ils correctement ?). La beauté de la chose est que la boucle de parcours est localisée à un seul endroit : toutes les features en héritent au lieu de la réimplémenter. Certaines features devaient mesurer les portails attachés à un espace : on reproduisit le pattern avec une classe PortalFeature, dérivée de SolutionSpaceFeature, dont l'implémentation de newSolutionSpace bouclait sur les portails et appelait à son tour une méthode abstraite newPortal.

abstract class PortalFeature extends SolutionSpaceFeature {
  // On réutilise la trame du parent, en y branchant un nouveau « trou ».
  protected newSolutionSpace(space: SolutionSpace): void {
    for (const portal of space.getPortals()) {
      this.newPortal(space, portal);
    }
  }
  protected abstract newPortal(space: SolutionSpace, portal: Portal): void;
}

Cette structure permit de créer des dizaines de features parcourant la structure du plan sans en connaître la forme. Si les détails de cette structure changeaient (par exemple, passer à des itérateurs standard), il suffisait de modifier deux classes au lieu de plusieurs dizaines.

Note

Pourquoi Template Method plutôt que Strategy ? Avec Strategy, le couplage aurait été bien plus lâche : un changement dans les algorithmes de parcours n'aurait touché que deux classes pilotes (les « Driver »), sans forcer la recompilation des features — alors qu'avec Template Method, modifier SpaceFeature et PortalFeature obligeait vraisemblablement à tout recompiler. De plus, Strategy respecte mieux le principe d'inversion des dépendances (DIP) que Template Method. Pourquoi alors Template Method ? Parce qu'il était plus simple ; parce que la structure de données ne changeait pas souvent ; et parce que tout recompiler ne coûtait que quelques minutes. Le surcroît de découplage de Strategy ne valait pas les deux classes supplémentaires qu'il imposait.

La conception du framework de délivrance

Les programmes de délivrance se recouvraient largement. L'écran avait partout la même structure : à gauche, une Command Window (« fenêtre de commande ») n'offrant qu'une colonne de boutons — « Placer », « Effacer », « Déplacer / Ajuster », « Zoom », « Terminé » ; à droite, une Task Window (« fenêtre de tâche »), vaste zone défilable et zoomable où l'utilisateur dessinait. Cliquer sur un bouton de commande déclenchait typiquement une interaction dans la fenêtre de tâche : pour placer une pièce, on cliquait sur « Placer », on choisissait un type de pièce dans un menu, puis on cliquait dans la fenêtre de tâche pour ancrer un coin, le coin opposé suivant la souris jusqu'au second clic. Ces gestes se ressemblaient — sans être identiques — d'une vignette à l'autre (certaines manipulaient non des pièces mais des courbes de niveau, des limites de propriété ou des toitures). Cette similarité offrait une formidable occasion de réutilisation. Le framework finit par atteindre près de 75 000 lignes ; nous n'en explorons ici que deux éléments représentatifs : le modèle d'événements et l'architecture Taskmaster.

Le modèle d'événements : le pattern State

Chaque action de l'utilisateur engendrait un événement nommé d'après le bouton ou l'item de menu. Le marshalling (l'aiguillage) de ces événements posait un vrai problème : le framework pouvait traiter une grande partie des événements, mais chaque vignette devait pouvoir redéfinir ce traitement pour un événement donné. Pire, la liste des événements n'était pas close : chaque vignette choisissait ses propres boutons et items de menu. Le framework devait donc aiguiller les événements communs tout en autorisant chaque vignette à surcharger le traitement par défaut et à aiguiller ses propres événements spécifiques.

Le mécanisme de conversion des événements en actions est le pattern State. Une portion de la machine à états finis qui aiguille les événements de la Command Window se lit ainsi : à l'état Idle, un événement Measure lance la tâche de mesure et transite vers Measuring ; un Erase lance la tâche d'effacement et transite vers Erasing ; l'événement ScreenCursor bascule le curseur sans changer d'état, et continue de fonctionner même pendant un effacement. Cliquer sur un bouton démarrant une autre tâche pendant un effacement annule la tâche en cours.

La structure statique répartit les classes en deux : CommandWindow, StandardCommandWindow et StandardFSM sont des classes du framework ; les autres sont spécifiques à la vignette. Les événements sont reçus par la VignetteCommandWindow, passés à la machine à états qui les traduit en actions, puis les actions reviennent vers la hiérarchie CommandWindow qui les exécute. CommandWindow fournit les implémentations des actions standard (comme measureTask et eraseTask), StandardCommandWindow aiguille les événements standard, et VignetteCommandWindow implémente et aiguille les événements spécifiques tout en pouvant redéfinir les comportements standard.

À retenir

Les classes d'état de cette machine portent le stéréotype « generated » : elles sont engendrées automatiquement par SMC, le State Machine Compiler. On lui fournit une description textuelle des transitions où, sous chaque état, une ligne donne l'événement déclencheur (par exemple Measure), l'état cible (Measuring) et l'action (MeasureTask), et SMC produit les classes du pattern State. Le code généré ne demande ni édition ni inspection. Le développeur n'écrit que la VignetteCommandWindow (avec ses événements et actions spécifiques), le VignetteFSMContext (qui déclare les interfaces) et la VignetteFSMGlue (qui réachemine les actions vers la VignetteCommandWindow) — plus la description de la FSM. Comme la plupart des vignettes se comportaient de façon voisine, une description de machine standard servait de modèle, légèrement adaptée pour chacune. Inconvénient assumé : les descriptions étant très similaires, une modification de la machine générique imposait des changements quasi identiques dans chaque fichier — pénible et propice aux erreurs.

Voici l'esprit de la machine d'événements transposé en TypeScript, où chaque état est une dérivée et où le contexte délègue à l'état courant :

// Une dérivée d'état par état de la machine, à la manière de ce que SMC engendre.
interface CommandWindowState {
  measure(fsm: VignetteFSM): void;
  erase(fsm: VignetteFSM): void;
}

class IdleState implements CommandWindowState {
  measure(fsm: VignetteFSM): void {
    fsm.setState(fsm.measuringState);
    fsm.measureTask(); // action standard, routée vers la CommandWindow
  }
  erase(fsm: VignetteFSM): void {
    fsm.setState(fsm.erasingState);
    fsm.startEraseTask();
  }
}

class VignetteFSM {
  readonly measuringState = new MeasuringState();
  readonly erasingState = new ErasingState();
  private state: CommandWindowState = new IdleState();

  constructor(private window: VignetteCommandWindow) {}

  setState(s: CommandWindowState): void { this.state = s; }

  // Les événements délèguent à l'état courant.
  measure(): void { this.state.measure(this); }
  erase(): void { this.state.erase(this); }

  // Les actions sont réacheminées vers la fenêtre (rôle de la « Glue »).
  measureTask(): void { this.window.measureTask(); }
  startEraseTask(): void { this.window.startEraseTask(); }
}

L'architecture Taskmaster

Une fois l'événement converti en action, comment l'action est-elle exécutée ? Sans surprise, le cœur de chaque action est lui aussi piloté par une machine à états. Prenons MeasureTask : l'utilisateur clique un premier point (un petit réticule apparaît), une ligne extensible suit la souris en affichant sa longueur, puis un second clic fige la mesure ; le cycle peut se répéter, et seule l'annulation par la Command Window y met fin. Sa machine débute par un init et occupe l'état GetFirstPoint ; les événements survenant dans la Task Window sont routés vers la tâche courante.

// Chaque tâche enferme sa propre FSM, ici aussi engendrée par SMC.
interface MeasureTaskState {
  movePoint(task: MeasureTask): void;
  gotPoint(task: MeasureTask): void;
}

class GetFirstPointState implements MeasureTaskState {
  movePoint(task: MeasureTask): void { /* rien, comme attendu */ }
  gotPoint(task: MeasureTask): void {
    task.recordStartPt();          // dessine le premier réticule, mémorise le point
    task.setState(task.getSecondPointState);
  }
}

class GetSecondPointState implements MeasureTaskState {
  movePoint(task: MeasureTask): void { task.dragLine(); } // ligne XOR + distance
  gotPoint(task: MeasureTask): void {
    task.recordEndPt();            // fige la mesure, affiche la distance
    task.setState(task.getFirstPointState);
  }
}

Une tâche plus complexe, le TwoPointBox (boîte à deux points), dessine un rectangle en deux clics : le premier ancre un coin, une boîte extensible suit la souris, le second clic la fige — mais via un état d'attente WaitingForUp pour ne valider qu'au relâchement du bouton, et un AddBox qui vérifie la validité de la boîte (dégénérée ? en conflit ?) avant de l'accepter ou de tout recommencer. Le framework contient des dizaines de telles tâches, chacune dérivée d'une classe Task enfermant sa propre machine à états, là encore engendrée par SMC.

L'architecture Taskmaster relie la Command Window à la Task Window et gère les tâches sélectionnées. Toutes les classes, jusqu'aux implémentations MeasureTaskImplementation et TwoPointBoxImplementation, appartiennent au framework ; mais ces implémentations sont abstraites : quelques fonctions comme addBox ou checkComplete y restent à charge de chaque vignette. Ainsi, les tâches du framework gouvernent l'essentiel des interactions de toutes les vignettes : pour dessiner une boîte, un développeur dérive TwoPointBoxImplementation ; pour placer un objet d'un seul clic, il redéfinit SinglePointPlacementTask ; pour dessiner à partir d'une polyligne, PolylineTask. Ces tâches gèrent les interactions et le glissement, et offrent à l'ingénieur les points d'ancrage pour valider et créer ses objets.

À retenir

  • Construire un framework réutilisable est difficile : le premier framework ETS, conçu en se focalisant sur une seule vignette sous la pression des délais, ne fut réutilisable que par copier-coller — un échec dont l'objet seul ne protège pas.
  • La stratégie gagnante fut de développer le framework concurremment avec plusieurs vignettes (au moins quatre), en ne versant au socle que ce qui avait été réutilisé avec succès — écho au conseil de Rebecca Wirfs-Brock : bâtir trois applications ou plus, puis les jeter, avant de croire son architecture juste.
  • Les gains de réutilisation furent spectaculaires : près des cinq sixièmes du code de chaque vignette venaient du framework, et la réécriture de Building Design passa de plus d'une année-homme à 2,5 mois-homme, soit une productivité multipliée par environ 6:1.
  • Le pattern Template Method (« écrire la boucle une seule fois ») élimina la duplication de dizaines de features parcourant la même structure : la boucle vit dans la classe de base, les dérivées ne remplissent que les trous (doEval, newSolutionSpace, newPortal).
  • Le choix Template Method vs Strategy est un arbitrage : Strategy offre un couplage plus lâche et respecte mieux le DIP, mais au prix de deux classes supplémentaires ; ici, la simplicité et la stabilité de la structure ont fait pencher pour Template Method.
  • Le pattern State, combiné au compilateur SMC, aiguille les événements (Command Window) comme les interactions (Taskmaster) : SMC engendre depuis une table textuelle des classes d'état qu'on n'a jamais à relire ni modifier, le développeur n'écrivant que la fenêtre, le contexte et la « glue » spécifiques.