Avez-vous une question? (216) 97 656 803 hajjriadh@gmail.com
Angular

TP 4 : Architecturer l'application sous forme d'une organisation de services

Un cours complet pour maîtriser TP 4 : Architecturer l'application sous forme d'une organisation de services.

0 chapitres 0 QCM Accès gratuit
Dans cet article
Durée Niveau Outils Livrable
6 heures (10 parties) Débutant, après le chapitre « L'injection de dépendances et les services » Node.js 22.18+, Angular CLI 22, VS Code, Git Dépôt Git avec un commit par partie, un README.md et des tests qui passent
  1. Objectifs

    • Organiser une application en couches: des composants qui affichent, des services qui possèdent les données et les règles, une configuration qui assemble le tout
    • Créer des services avec @Service(), garder leur état dans des signals en lecture seule, et les demander avec inject()
    • Faire dépendre un service d'une abstraction (classe abstraite) et choisir l'implémentation avec useClass
    • Fournir des valeurs avec des InjectionToken: valeur par défaut, useValue, une fonction remplaçable (l'horloge)
    • Limiter la portée d'un service à un composant avec providers, et le recevoir dans un enfant avec inject(…, { optional: true })
    • Rendre l'application extensible avec un jeton multi, puis tout regrouper dans une fonction provideMediatheque(withXxx())
    • Tester les services avec TestBed, en remplaçant le stockage et la date du jour
  1. Prérequis

  1. Présentation du projet

    • Vous allez développer la Médiathèque du lycée, l'application de prêt de livres du CDI :
      • un tableau de bord: nombre de livres, livres disponibles, emprunts en cours et emprunts en retard;
      • deux guichets côte à côte : chaque bibliothécaire choisit un adhérent, recherche des livres, compose sa sélection et valide l'emprunt ;
      • la liste des emprunts en cours, triée par date de retour, avec un bouton « Rendre » ;
      • des règles: un livre ne peut être prêté deux fois, trois livres au maximum par adhérent, un adhérent en retard est bloqué, une seule bande dessinée à la fois ;
      • des notifications pour chaque prêt, retour ou refus, et une sauvegarde dans le navigateur.
    • La difficulté n'est pas l'interface : c'est l'organisation. Au TP 3, App possédait tout et les données voyageaient d'input en output. Ici, aucun composant ne possède de données : ce sont les services.

Image de référence : la médiathèque terminée, avec deux guichets

Architecture de la médiathèque : composants, services et configuration

  1. Organisation du TP

    • Le TP est découpé en dix parties. Chacune indique sa durée, les notions mobilisées, les consignes, une aide, un point de contrôle à vérifier avant de continuer, et des questions.
    • Répondez aux questions dans un fichier README.md, à la racine du projet, au fur et à mesure.
    • À la fin de chaque partie, faites un commit: git commit -m "Partie N : …".
    • Les aides donnent des squelettes de code, jamais la solution complète : c'est à vous de compléter.
    • Trois règles s'appliquent à tout le TP : un composant ne contient aucune donnée métier (seulement de l'état d'affichage, comme un texte recherché) ; un service n'expose que des signals en lecture seule et des méthodes ; aucun new d'un service : tout passe par inject().

Déroulement du TP sur 6 heures

Notions du chapitre réinvesties dans chaque partie du TP

  1. Partie 0 : mise en place du projet

⏱ 20 min

Notions mobilisées : Les composants · Les services Angular

  • Créez le projet sans routage, générez les cinq composants et les trois dossiers de l'architecture :
npx @angular/cli@latest new tp4-mediatheque --defaults --ssr=false --style=css --routing=false
cd tp4-mediatheque
npx ng generate component tableau-bord --skip-tests
npx ng generate component guichet --skip-tests
npx ng generate component liste-livres --skip-tests
npx ng generate component emprunts-en-cours --skip-tests
npx ng generate component zone-notifications --skip-tests
mkdir src/app/modeles src/app/services src/app/config
npx ng serve
  • Dans app.html, gardez un en-tête (<h1>Médiathèque du lycée</h1>) et un <main> vide.
  • Dans styles.css, reprenez les variables de couleur et le style des boutons du TP 3, et ajoutez une classe .badge avec trois variantes : .libre (vert), .pris (gris) et .retard (rouge).
  • Dans app.config.ts, activez le français pour le pipe date: registerLocaleData(localeFr) et { provide: LOCALE_ID, useValue: 'fr' }.
src/app/
├── modeles/    ← types et données d'exemple (partie 1) : aucun code Angular
├── services/   ← l'état et la logique : Catalogue, Adherents, Emprunts… (parties 2 à 6)
├── config/     ← jetons, règles, provideMediatheque() (parties 4 à 7)
└── tableau-bord/  guichet/  liste-livres/  emprunts-en-cours/  zone-notifications/

✅ Point de contrôle

  • ng serve fonctionne, la page affiche le titre « Médiathèque du lycée ».
  • La console du navigateur (F12) ne contient aucune erreur.

❓ Questions (à répondre dans le README.md)

  1. Pourquoi séparer modeles/, services/ et config/ au lieu de tout ranger à côté des composants ?
  2. Que contient le fichier créé par ng generate service avec Angular 22 ? Quel décorateur y trouve-t-on ?
git add .
git commit -m "Partie 0 : création du projet et des composants"
  1. Partie 1 : le modèle et les données

⏱ 30 min

Notions mobilisées : Le typage fort

  • Dans src/app/modeles/mediatheque.ts, sans aucun code Angular :
    • un type Genre ('roman', 'bd', 'science', 'histoire') ;
    • quatre interfaces dont toutes les propriétés sont readonly: Livre (id, titre, auteur, genre), Adherent (id, nom, classe), Emprunt (id, livreId, adherentId, le, retour, les deux dates au format AAAA-MM-JJ) et Reglement (maxEmprunts, dureeJours) ;
    • deux fonctions : dateDuJour() et ajouterJours(date, jours), qui s'appuient sur une fonction privée formater(d: Date).
  • Dans src/app/modeles/donnees.ts: les constantes LIVRES (8 livres, dont 2 bandes dessinées) et ADHERENTS (4 élèves), et une fonction empruntsExemples(aujourdhui) qui renvoie deux emprunts datés par rapport au jour donné : l'un en retard de 6 jours, l'autre à rendre dans 11 jours.
export function ajouterJours(date: string, jours: number): string {
  const d = new Date(`${date}T12:00:00`);
  /* à compléter : avancer de « jours » jours, puis formater */
}

// donnees.ts
export function empruntsExemples(aujourdhui: string): Emprunt[] {
  return [
    { id: 'e1', livreId: 'l2', adherentId: 'a1', le: ajouterJours(aujourdhui, -20), retour: /* en retard */ },
    /* à compléter */
  ];
}
  • Testez vos fonctions sans navigateur : Node.js 22 exécute directement un fichier TypeScript sans import.
node --no-warnings --input-type=module -e "import { ajouterJours, dateDuJour } from './src/app/modeles/mediatheque.ts'; console.log(dateDuJour(), '|', ajouterJours('2026-12-28', 7), '|', ajouterJours('2026-03-01', -1));"
2026-11-01 | 2027-01-04 | 2026-02-28

✅ Point de contrôle

  • ajouterJours passe correctement d'un mois et d'une année à l'autre (résultats ci-dessus).
  • empruntsExemples(dateDuJour()) renvoie un emprunt dont la date de retour est passée, et un dont elle est à venir.

❓ Questions (à répondre dans le README.md)

  1. Pourquoi ajouterJours crée-t-il la date à T12:00:00 plutôt qu'à minuit ?
  2. Pourquoi les emprunts d'exemple sont-ils produits par une fonction qui reçoit la date du jour, et non par une constante ?
git add .
git commit -m "Partie 1 : modèle et données d'exemple"
  1. Partie 2 : un premier service, le catalogue

⏱ 40 min

Notions mobilisées : Les services Angular · Injecter un service Angular · Les signals

  • Générez le service : npx ng g s services/catalogue --skip-tests. La classe Catalogue, décorée par @Service(), est disponible dans toute l'application, avec une seule instance.
    • un signal privé _livres, initialisé avec LIVRES, et sa version publique en lecture seule : livres = this._livres.asReadonly();
    • un computed nombre; une méthode trouver(id); une méthode rechercher(texte) qui filtre sur le titre et l'auteur, sans tenir compte des majuscules, et renvoie tous les livres si le texte est vide.
  • ListeLivres obtient le catalogue avec inject(Catalogue). Le texte recherché est un état d'affichage: il reste dans le composant (signal recherche), et un computed livres appelle catalogue.rechercher(). Affichez chaque livre (titre et auteur), et « Aucun livre ne correspond. » avec @empty.
  • Dans App, placez temporairement la liste dans une carte « Catalogue ».
@Service()
export class Catalogue {
  private readonly _livres = signal<readonly Livre[]>(LIVRES);
  readonly livres = this._livres.asReadonly();
  readonly nombre = computed(() => /* à compléter */);

  rechercher(texte: string): readonly Livre[] { /* à compléter */ }
}

export class ListeLivres {
  private readonly catalogue = inject(Catalogue);
  protected readonly recherche = signal('');
  protected readonly livres = computed(() => /* à compléter */);
}

Point de contrôle de la partie 2 : le catalogue affiché par un service

✅ Point de contrôle

  • Les huit livres s'affichent ; taper « hu » dans la recherche ne laisse que « Les Misérables » (Victor Hugo).
  • Aucun composant ne contient le tableau des livres : il n'existe que dans Catalogue.
⚠ Piège : un @Service ne reçoit ses dépendances que par inject(). Un constructeur avec paramètre est refusé à la compilation : NG2028: @Service class cannot use constructor dependency injection. Use the `inject` function instead. Et inject() appelé dans une méthode (après la construction) donne NG0203: The `Emprunts` token injection failed. `inject()` function must be called from an injection context…

❓ Questions (à répondre dans le README.md)

  1. Pourquoi le signal _livres est-il privé, et exposé avec asReadonly()? Que se passerait-il si un composant pouvait appeler set()?
  2. Si deux composants font inject(Catalogue), reçoivent-ils le même objet ? Comment le vérifier ?
  3. Pourquoi le texte recherché reste-t-il dans ListeLivres et non dans Catalogue?
git add .
git commit -m "Partie 2 : service Catalogue"
  1. Partie 3 : un stockage abstrait et le service des emprunts

⏱ 45 min

Notions mobilisées : Principes de l'injection de dépendances · Définir un provider · Configurer l'injecteur

  • Le stockage, dans services/stockage.ts:
    • une classe abstraite Stockage, avec deux méthodes abstraites : lire<T>(cle, defaut) et ecrire<T>(cle, valeur): c'est le contrat;
    • deux implémentations, décorées par @Service({ autoProvided: false }): StockageLocal (dans localStorage, clés préfixées par mediatheque., dans un try … catch) et StockageMemoire (dans une Map, pour les tests) ;
    • dans app.config.ts: { provide: Stockage, useClass: StockageLocal }.
  • Les adhérents: un service Adherents, sur le modèle de Catalogue (signal privé, liste en lecture seule, trouver(id)).
  • Les emprunts, service Emprunts:
    • il demande inject(Stockage), jamais StockageLocal; sa liste démarre avec stockage.lire('emprunts', empruntsExemples(dateDuJour())), et un effect, dans le constructeur, l'enregistre à chaque changement ;
    • méthodes : estDisponible(livreId), estEnRetard(emprunt), emprunter(livreId, adherentId) (refusé si le livre n'est pas disponible ; retour prévu dans 14 jours) et rendre(id).
  • EmpruntsEnCours affiche un tableau (livre, adhérent, retour prévu avec date: 'd MMM', badge « En retard », bouton « Rendre ») à partir d'un computed lignes qui trie les emprunts par date de retour et les complète avec le titre et le nom.
  • ListeLivres injecte aussi Emprunts et affiche un badge « Disponible » ou « Emprunté ».
export abstract class Stockage {
  abstract lire<T>(cle: string, defaut: T): T;
  abstract ecrire<T>(cle: string, valeur: T): void;
}

@Service()
export class Emprunts {
  private readonly stockage = inject(Stockage);   // l'abstraction, pas l'implémentation
  private readonly _liste = signal<readonly Emprunt[]>(/* à compléter */);
  readonly liste = this._liste.asReadonly();

  constructor() {
    effect(() => /* enregistrer la liste */);
  }
}

Point de contrôle de la partie 3 : le catalogue et les emprunts en cours

✅ Point de contrôle

  • Deux emprunts s'affichent : « L'Étranger » (Amel), en rouge et « En retard », et « Les Aventures de Tintin » (Karim).
  • « Rendre » sur Tintin le retire du tableau, et le catalogue le marque aussitôt « Disponible » : deux composants, un seul service.
  • Après F5, l'emprunt rendu n'est pas revenu : la liste est bien enregistrée.
⚠ Piège : oublier le provider de Stockage laisse une page vide, et la console indique : NG0201: No provider found for `Stockage`. Source: Environment Injector. Path: Emprunts -> Stockage. Une classe abstraite n'est pas un service : il faut dire à Angular quelle classe utiliser. Et useClass: Stockage est refusé : TS2322 … Cannot assign an abstract constructor type to a non-abstract constructor type.

❓ Questions (à répondre dans le README.md)

  1. Quel est l'intérêt de faire dépendre Emprunts de Stockage plutôt que de localStorage? Citez deux situations où l'on changerait d'implémentation.
  2. Pourquoi StockageLocal est-il décoré par @Service({ autoProvided: false }) et non par @Service()?
  3. Pourquoi l'enregistrement est-il un effect et non un appel à ecrire() dans chaque méthode ?
git add .
git commit -m "Partie 3 : stockage abstrait et emprunts"
  1. Partie 4 : des jetons pour la configuration, et le tableau de bord

⏱ 40 min

Notions mobilisées : Définir un provider · Configurer l'injecteur

  • Emprunts contient deux valeurs « en dur » : la durée (14 jours) et la date du jour (dateDuJour()). Impossible de les changer sans modifier le service, et impossible de tester un retard. Dans config/jetons.ts:
    • REGLEMENT, un InjectionToken<Reglement> dont la factory fournit la valeur par défaut : 3 livres, 14 jours ;
    • HORLOGE, un InjectionToken<() => string>: une fonction qui donne la date du jour ; par défaut, dateDuJour.
  • Dans Emprunts: inject(REGLEMENT) et inject(HORLOGE); remplacez chaque dateDuJour() par this.horloge() et 14 par this.reglement.dureeJours. Ajoutez un computed enRetard.
  • TableauBord affiche quatre tuiles : livres, disponibles, emprunts, en retard (fond rouge s'il y en a). Il n'a aucune entrée: il injecte Catalogue et Emprunts.
export const HORLOGE = new InjectionToken<() => string>('HORLOGE', {
  factory: () => /* à compléter */,
});

// Expérience, à retirer ensuite : dans app.config.ts
{ provide: HORLOGE, useValue: () => '2026-12-31' },

Point de contrôle de la partie 4 : avec l'horloge réglée au 31 décembre, les deux emprunts sont en retard

✅ Point de contrôle

  • Le tableau de bord indique 8 livres, 6 disponibles, 2 emprunts, 1 en retard.
  • Expérience: ajoutez le provider HORLOGE ci-dessus et rechargez. Les emprunts enregistrés n'ont pas changé, mais l'application « se croit » le 31 décembre : les deux sont en retard. Retirez ensuite ce provider.
⚠ Piège : TypeScript ne vérifie pas que useValue correspond au type du jeton : { provide: HORLOGE, useValue: '2026-12-31' } (une chaîne au lieu d'une fonction) compile, mais this.horloge() lève TypeError: this.horloge is not a function.

❓ Questions (à répondre dans le README.md)

  1. Pourquoi HORLOGE fournit-il une fonction et non directement la date du jour (une chaîne) ? (indice : une application qui reste ouverte après minuit)
  2. TableauBord n'a aucun input(). Comparez avec le TP 3 : qu'est-ce qui a remplacé les entrées ?
  3. Pourquoi ne faut-il pas mettre de provider pour REGLEMENT dans app.config.ts pour que l'application fonctionne ?
git add .
git commit -m "Partie 4 : jetons REGLEMENT et HORLOGE, tableau de bord"
  1. Partie 5 : un service par guichet

⏱ 45 min

Notions mobilisées : Configurer l'injecteur · Injecter un service Angular

  • Deux bibliothécaires travaillent en même temps : chacun a sa propre sélection. Générez services/selection et décorez-le par @Service({ autoProvided: false }): il n'existe pas à la racine.
    • Selection: un signal adherentId (chaîne vide au départ), la liste des identifiants choisis (privée, en lecture seule), nombre, contient(id), basculer(id) et vider();
    • Guichet déclare providers: [Selection]: chaque guichet crée sa propre instance. Il a une entrée numero, une liste déroulante des adhérents, une <app-liste-livres />, le résumé de la sélection et un bouton « Valider l'emprunt » (désactivé sans adhérent ou sans livre) ;
    • valider() appelle emprunts.emprunter() pour chaque livre choisi, puis vide la sélection ;
    • ListeLivres demande inject(Selection, { optional: true }): dans un guichet, elle reçoit la sélection de son guichet et affiche un bouton « + » / « ✓ » (désactivé si le livre est emprunté) ; ailleurs, elle reçoit null et n'affiche pas de bouton.
  • Dans App, remplacez la carte « Catalogue » par deux guichets : <app-guichet [numero]="1" /> et <app-guichet [numero]="2" />.

Arbre des injecteurs : une Selection par guichet, un seul catalogue

@Component({
  selector: 'app-guichet',
  imports: [ListeLivres],
  providers: [Selection],      // une instance par guichet
  /* … */
})
export class Guichet {
  protected readonly selection = inject(Selection);
  private readonly emprunts = inject(Emprunts);
  protected valider(): void { /* à compléter */ }
}

Point de contrôle de la partie 5 : deux guichets, deux sélections indépendantes

✅ Point de contrôle

  • Choisir des livres au guichet 1 ne change rien au guichet 2.
  • Valider au guichet 1 : les livres deviennent « Emprunté » dans les deux guichets, apparaissent dans le tableau, et le tableau de bord se met à jour.
⚠ Piège : sans providers: [Selection] dans Guichet, la page reste vide : NG0201: No provider found for `Selection`. Source: Environment Injector. Sans { optional: true }, la même erreur apparaît dès qu'une ListeLivres est placée hors d'un guichet.

❓ Questions (à répondre dans le README.md)

  1. Combien d'instances de Selection, de Emprunts et de Catalogue existent quand la page affiche deux guichets ?
  2. Comment ListeLivres trouve-t-elle la sélection de son guichet, alors qu'elle ne la reçoit pas en input()?
  3. Que se passerait-il si Selection était décorée par @Service(), sans providers dans Guichet?
git add .
git commit -m "Partie 5 : guichets et sélection par composant"
  1. Partie 6 : notifications et règles d'emprunt (multi)

⏱ 45 min

Notions mobilisées : Définir un provider · Les services Angular

  • Notifications: un service avec une liste de messages (id, texte, type: 'succes' ou 'erreur'), des méthodes succes(), erreur() et fermer(id), chaque message disparaissant après 4 secondes. ZoneNotifications les affiche en bas à droite.
  • Les règles: au lieu d'empiler des if dans emprunter(), chaque règle est une fonction qui reçoit une DemandeEmprunt et renvoie un message de refus, ou null:
    • dans le modèle : l'interface DemandeEmprunt (livre, adherent, disponible, enCours: les livres déjà empruntés, retards, reglement) et le type RegleEmprunt;
    • dans config/jetons.ts: REGLES_EMPRUNT, un InjectionToken<readonly RegleEmprunt[]>;
    • dans config/regles.ts: regleDisponible, regleLimite (selon reglement.maxEmprunts) et regleRetard;
    • dans app.config.ts: un provider par règle, avec multi: true.
  • Emprunts.emprunter() construit la demande, applique toutes les règles (regles.map(…).find(…)), notifie le premier refus, sinon crée l'emprunt et notifie « « … » prêté à … ». rendre() notifie « « … » est de retour. ».

Les règles d'emprunt fournies par un jeton multi

// app.config.ts
{ provide: REGLES_EMPRUNT, useValue: regleDisponible, multi: true },
{ provide: REGLES_EMPRUNT, useValue: regleLimite, multi: true },
{ provide: REGLES_EMPRUNT, useValue: regleRetard, multi: true },

// emprunts.ts
const refus = this.regles.map((regle) => regle(demande)).find(/* à compléter */);

Point de contrôle de la partie 6 : un prêt accepté, un prêt refusé

✅ Point de contrôle

  • Un prêt à Sana affiche une notification verte. Un prêt à Amel est refusé : « Amel Ben Salah doit d'abord rendre un livre en retard. »
  • Après avoir rendu « L'Étranger », Amel peut emprunter. Un quatrième livre pour le même adhérent est refusé (« … a déjà 3 livre(s) : c'est le maximum. »).
⚠ Piège : oublier multi: true sur un seul des providers fait échouer le démarrage : Error: Cannot mix multi providers and regular providers. Et sans aucun provider, NG0201: No provider found for `InjectionToken REGLES_EMPRUNT`.

❓ Questions (à répondre dans le README.md)

  1. Pour ajouter une règle, quels fichiers faut-il modifier ? Lequel ne change jamais?
  2. Pourquoi les règles reçoivent-elles une DemandeEmprunt toute prête, au lieu d'injecter elles-mêmes Emprunts?
  3. Pourquoi Notifications est-il injecté dans Emprunts, et pas appelé par le composant Guichet?
git add .
git commit -m "Partie 6 : notifications et règles multi"
  1. Partie 7 : une fonction provideMediatheque(withXxx())

⏱ 40 min

Notions mobilisées : Définir un provider · Configurer l'injecteur

  • app.config.ts contient maintenant quatre providers techniques. Comme provideRouter() ou provideHttpClient(), regroupez-les dans config/provide-mediatheque.ts:
    • provideMediatheque(...fonctionnalites) renvoie makeEnvironmentProviders([...]): d'abord les valeurs par défaut (StockageLocal, les trois règles), puis les fonctionnalités, aplaties avec flat();
    • withReglement(choix: Partial<Reglement>) complète le règlement par défaut et vérifie les options : maxEmprunts entier ≥ 1, dureeJours entre 1 et 60, sinon une Error au message clair ;
    • withStockage('local' | 'memoire') choisit l'implémentation avec useClass;
    • withRegle(regle) ajoute une règle (multi: true).
  • Ajoutez dans regles.ts une quatrième règle, regleUneBd: une seule bande dessinée à la fois. app.config.ts se réduit à :
providers: [
  provideBrowserGlobalErrorListeners(),
  { provide: LOCALE_ID, useValue: 'fr' },
  provideMediatheque(withReglement({ maxEmprunts: 3, dureeJours: 14 }), withRegle(regleUneBd)),
],

provideMediatheque : les valeurs par défaut, puis les fonctionnalités choisies

Point de contrôle de la partie 7 : la règle ajoutée refuse une deuxième bande dessinée

✅ Point de contrôle

  • L'application fonctionne exactement comme avant. Sana choisit « Astérix » et « Tintin » : le premier est prêté, le second refusé (« Une seule bande dessinée à la fois pour Sana Gharbi. »).
  • withReglement({ maxEmprunts: 0 }) empêche le démarrage, et la console affiche : withReglement : maxEmprunts doit être un entier supérieur ou égal à 1 (reçu 0). Remettez 3.
⚠ Piège : provideMediatheque() renvoie des EnvironmentProviders: ils se placent dans app.config.ts (ou les providers d'une route), jamais dans les providers d'un composant : TS2322: Type 'EnvironmentProviders' is not assignable to type 'Provider'.

❓ Questions (à répondre dans le README.md)

  1. Dans provideMediatheque, pourquoi les valeurs par défaut sont-elles placées avant les fonctionnalités ?
  2. Pourquoi vérifier les options dans withReglement, au démarrage, plutôt que lors d'un emprunt ?
  3. Que gagne un collègue qui reprend votre projet, en lisant app.config.ts?
git add .
git commit -m "Partie 7 : provideMediatheque"
  1. Partie 8 : tester les services

⏱ 35 min

Notions mobilisées : Définir un provider · Injecter un service Angular

  • L'organisation en services rend l'application testable sans afficher un seul composant. Écrivez deux fichiers de test :
    • services/emprunts.spec.ts: une fonction creer(...fonctionnalites) configure TestBed avec provideMediatheque(withStockage('memoire'), ...) et une horloge fixe ({ provide: HORLOGE, useValue: () => '2026-11-02' }), puis renvoie TestBed.inject(Emprunts);
    • six tests : les deux emprunts d'exemple (dont un en retard), la date de retour selon le règlement, le refus d'un livre déjà emprunté (avec le message de la notification), la limite, le blocage d'un adhérent en retard, le retour d'un livre ;
    • config/provide-mediatheque.spec.ts: quatre tests sur les trois règles par défaut, withRegle, withStockage et le refus d'un règlement invalide (toThrowError).
function creer(...fonctionnalites: FonctionnaliteMediatheque[]): Emprunts {
  TestBed.configureTestingModule({
    providers: [
      provideMediatheque(withStockage('memoire'), ...fonctionnalites),
      { provide: HORLOGE, useValue: () => '2026-11-02' },
    ],
  });
  return TestBed.inject(Emprunts);
}

it('calcule la date de retour avec le règlement', () => {
  const emprunts = creer(withReglement({ dureeJours: 7 }));
  expect(emprunts.emprunter('l1', 'a3')).toBe(true);
  expect(emprunts.liste().at(-1)!.retour).toBe(/* à compléter */);
});

Point de contrôle de la partie 8 : les dix tests passent

✅ Point de contrôle

  • npx ng test --watch=false affiche 10 tests réussis (ajoutez --reporters=verbose pour voir le nom de chacun).
  • Les tests passent quel que soit le jour où on les lance, et ne laissent rien dans localStorage.

❓ Questions (à répondre dans le README.md)

  1. Sans le jeton HORLOGE, comment écririez-vous le test « bloque un adhérent qui a un livre en retard » ? Pourquoi serait-il fragile ?
  2. Pourquoi les tests utilisent-ils withStockage('memoire')?
  3. Le provider HORLOGE est placé après provideMediatheque(). Est-ce important ici ? Pourquoi ?
git add .
git commit -m "Partie 8 : tests des services"
  1. Partie 9 : variantes et finitions

⏱ 20 min

  • Choisissez deux variantes (chaque binôme annonce ses choix au formateur) :
    • un service Historique qui garde les dix derniers prêts et retours, affiché sous le tableau ;
    • une règle « pas d'emprunt le vendredi après 16 h », ajoutée avec withRegle() et testée avec une horloge fixe ;
    • un bouton « Prolonger » (+7 jours, une seule fois), avec la règle correspondante ;
    • un troisième guichet, et une option withGuichets(n);
    • garder dans la sélection les livres refusés, pour que le bibliothécaire voie ce qui n'a pas été prêté ;
    • une variante libre, validée par le formateur.
  • Relisez votre code : aucun tableau de données dans un composant, aucun new de service, signals publics en lecture seule, aucun console.log, aucune erreur dans la console.
git add .
git commit -m "Partie 9 : variantes et finitions"
  1. Grille d'évaluation (sur 20)

Critère Points
Parties 0–1 : projet organisé en dossiers, modèle typé (readonly), fonctions de date 2
Partie 2 : service Catalogue (@Service(), signal privé, asReadonly()), inject() 2
Partie 3 : stockage abstrait, useClass, service Emprunts, sauvegarde par effect 3
Partie 4 : jetons REGLEMENT et HORLOGE, tableau de bord sans entrée 2
Partie 5 : Selection fournie par Guichet, optional, deux guichets indépendants 3
Partie 6 : notifications, règles avec un jeton multi 2
Partie 7 : provideMediatheque(withXxx()), vérification des options 2
Partie 8 : tests des services, horloge et stockage remplacés 2
README : réponses aux questions et description des variantes 2
Total 20
  1. Livrables

    • Le lien du dépôt Git, avec au moins dix commits nommés « Partie N : … ».
    • Le fichier README.md: vos noms, les réponses aux questions, les deux variantes choisies, une capture d'écran de l'application et une du résultat des tests.
    • Le projet doit se lancer avec npm install puis npx ng serve, et npx ng test --watch=false doit réussir.
  1. Erreurs fréquentes

Message ou symptôme Cause Solution
NG2028: @Service class cannot use constructor dependency injection. Use the `inject` function instead. Dépendance passée au constructeur d'un @Service private readonly x = inject(X);
NG0203: The `Emprunts` token injection failed. `inject()` function must be called from an injection context… inject() appelé dans une méthode L'appeler dans un champ de la classe
NG0201: No provider found for `Stockage`. Classe abstraite sans provider { provide: Stockage, useClass: StockageLocal } ou provideMediatheque()
TS2322 … Cannot assign an abstract constructor type to a non-abstract constructor type. useClass: Stockage Donner une implémentation concrète
NG0201: No provider found for `Selection`. providers: [Selection] oublié, ou ListeLivres hors d'un guichet Ajouter le provider ; inject(Selection, { optional: true })
Error: Cannot mix multi providers and regular providers multi: true oublié sur une règle Le mettre sur tous les providers du jeton
TS2322: Type 'EnvironmentProviders' is not assignable to type 'Provider'. provideMediatheque() dans un composant Le placer dans app.config.ts
TS2339: Property 'push' does not exist on type 'readonly string[]'. Modification d'un signal exposé en lecture seule Passer par une méthode du service (basculer())
withReglement : maxEmprunts doit être un entier supérieur ou égal à 1 (reçu 0)., page blanche Option refusée par la vérification Corriger la valeur dans app.config.ts
Les deux guichets partagent la même sélection Selection fournie à la racine (@Service()) autoProvided: false et providers dans Guichet
Un test passe aujourd'hui et échoue demain Date du jour lue directement Injecter HORLOGE et la fixer dans le test
Les tests voient les emprunts d'une autre exécution StockageLocal utilisé dans les tests withStockage('memoire')
  1. Pour aller plus loin

    • Charger le catalogue depuis une API avec HttpClient: seul Catalogue change, les composants ne voient rien.
    • Afficher les guichets, le catalogue et les statistiques sur des pages différentes, avec le routage: c'est le chapitre suivant.
    • Fournir un règlement différent par page avec les providers d'une route.
  1. Bilan

    • Les composants affichent, les services possèdent les données et les règles, la configuration assemble: chaque couche peut changer sans toucher aux autres.
    • Un service exposé en lecture seule protège son état : on ne le modifie que par ses méthodes.
    • Dépendre d'une abstraction (Stockage, HORLOGE) permet de changer d'implémentation… et de tester.
    • La portée d'un service se choisit : une instance pour l'application (@Service()), ou une par composant (providers).
    • Un jeton multi et une fonction provideXxx(withXxx()) rendent l'application extensible avec une seule ligne de configuration.
Une bonne architecture ne se voit pas à l'écran : elle se voit le jour où il faut changer le stockage, ajouter une règle ou écrire un test, et que cela ne demande qu'une ligne.