• 10 heures
  • Moyenne

Ce cours est visible gratuitement en ligne.

course.header.alt.is_video

course.header.alt.is_certifying

J'ai tout compris !

Mis à jour le 12/11/2024

Centralisez votre logique avec les Services

Dans le chapitre précédent, vous avez créé un service qui contient tous vos FaceSnaps, et que vous utilisez dans FaceSnapListComponent.

Cependant, dans une application totalement dynamique, on peut imaginer que ces FaceSnaps viendraient d'un serveur, ou d'une autre partie de l'application, et qu'il faudrait appeler une méthode pour les récupérer. D'ailleurs, toute modification d'un FaceSnap entraînerait également un appel au serveur.

Il faudra donc centraliser toutes les interactions avec les FaceSnaps dans FaceSnapsService, et c'est exactement ce que vous allez faire maintenant !

Centralisez les interactions

La seule vraie "interaction" que vous permettez actuellement à vos utilisateurs est le fait de snap un FaceSnap. Pour l'instant, tout se passe à l'intérieur de FaceSnapComponent. Personne à l'extérieur du component ne sait que quelque chose a changé. Une vraie implémentation de cette fonctionnalité ferait certainement un appel au backend pour augmenter le nombre de  snaps  dans la base de données, donc, comme pour toutes les interactions, il faut la faire passer par le service.

Pour l'instant, vous n'avez aucun moyen d'identifier un FaceSnap directement. Ajoutez dès maintenant une propriété obligatoire  id  de type  string  à votre modèle FaceSnap, et de le générer automatiquement à la création de chaque FaceSnap :

export class FaceSnap {

  location?: string;
  id: string;

  constructor(public title: string,
              public description: string,
              public imageUrl: string,
              public createdAt: Date,
              public snaps: number) {
    this.id = crypto.randomUUID().substring(0, 8);
  }
  //... 
}

Cet identifiant unique va vous permettre de snap un FaceSnap par son identifiant !

Prenez quelques instants pour réfléchir à comment vous implémenteriez une méthode pour ça.

Trouvé ? Voici une solution possible :

snapFaceSnapById(faceSnapId: string): void {
    const foundFaceSnap = this.faceSnaps.find(faceSnap => faceSnap.id === faceSnapId);
    if (!foundFaceSnap) {
      throw new Error('FaceSnap not found!');
    }
    foundFaceSnap.addSnap();
}

Cette méthode :

  • cherche un FaceSnap par son  id  dans le tableau faceSnaps avec la fonction  find()  ;

  • si le FaceSnap n'existe pas, on throw une erreur

  • s'il existe, on appelle sa méthode  addSnap()

Pour tester cette méthode, il faudra injecter FaceSnapsService dans FaceSnapComponent. Vous vous souvenez comment faire ?

constructor(private faceSnapsService: FaceSnapsService) {}

Du coup, il faut appeler la méthode que vous venez de créer dans  onSnap()  :

snap() {
    this.faceSnapsService.snapFaceSnapById(this.faceSnap.id);
    this.snapButtonText = 'Oops, unSnap!';
    this.userHasSnapped = true;
}

Il ne reste plus qu'à implémenter une méthode pour "unsnap" ! On pourrait très bien faire :

unsnapFaceSnapById(faceSnapId: string): void {
    const foundFaceSnap = this.faceSnaps.find(faceSnap => faceSnap.id === faceSnapId);
    if (!foundFaceSnap) {
      throw new Error('FaceSnap not found!');
    }
    foundFaceSnap.removeSnap();
}

Là, votre Spider-sense devrait être en train de crier. On vient d'écrire deux fois la même méthode avec une seule ligne qui change.

Alors comment remédier à ça ? Comment refactoriser ce code pour avoir quelque chose de propre ?

Je vous propose une solution qui profitera d'un outil génial fourni par TypeScript : les literal types.

Précisez les types avec les Literal Types

Voici une première idée :

snapFaceSnapById(faceSnapId: string, snapType: string): void {
    const foundFaceSnap = this.faceSnaps.find(faceSnap => faceSnap.id === faceSnapId);
    if (!foundFaceSnap) {
      throw new Error('FaceSnap not found!');
    }
    if (snapType === 'snap') {
        foundFaceSnap.addSnap();
    }
    if (snapType === 'unsnap') {
        foundFaceSnap.removeSnap();
    }
}

C'est pas mal, mais j'y vois deux inconvénients :

  • on pourrait passer n'importe quelle chaîne de caractères à cette méthode

  • on a de la logique "métier" dans notre service qui serait mieux gérée par notre classe FaceSnap

Afin de limiter les possibilités à des options sémantiques, on peut remplacer le type  string  par un literal type.

Dans votre dossier models, créez un fichier  snap-type.type.ts  :

export type SnapType = 'snap' | 'unsnap';

Vous pouvez ensuite exiger ce type comme argument à snapFaceSnapById :

snapFaceSnapById(faceSnapId: number, snapType: SnapType): void {

Ainsi, vous ne pourrez passer que 'snap' ou 'unsnap' comme deuxième argument. Non seulement votre IDE vous préviendra si vous essayez de passer autre chose, mais l'autocomplétion et la documentation automatique faciliteront l'utilisation de cette méthode :

Mon IDE me spécifie qu'il faut que je passe 'snap' ou 'unsnap'
Propositions IDE

Avec ça, dans FaceSnapComponent, l'implémentation devient :

unSnap() {
  this.faceSnapsService.snapFaceSnapById(this.faceSnap.id, 'unsnap');
  this.snapButtonText = 'Oh Snap!';
  this.userHasSnapped = false;
}

snap() {
  this.faceSnapsService.snapFaceSnapById(this.faceSnap.id, 'snap');
  this.snapButtonText = 'Oops, unSnap!';
  this.userHasSnapped = true;
}

Enfin, pour déplacer la logique "métier" d'interpréter le SnapType vers notre classe FaceSnap, je vous propose d'y créer une méthode snap() :

snap(snapType: SnapType) {
    if (snapType === 'snap') {
      this.addSnap();
    } else if (snapType === 'unsnap') {
      this.removeSnap();
    }
}

Ça nous laisse même la possibilité d'ajouter un nouveau SnapType à l'avenir sans difficulté !

Du coup, il ne reste plus qu'à y déléguer la gestion depuis le service :

snapFaceSnapById(faceSnapId: string, snapType: SnapType): void {
    const foundFaceSnap = this.faceSnaps.find(faceSnap => faceSnap.id === faceSnapId);
    if (!foundFaceSnap) {
      throw new Error('FaceSnap not found!');
    }
    foundFaceSnap.snap(snapType);
 }

Et toutes les interactions avec les FaceSnap passent par le service !

En résumé

  • Centraliser les interactions dans un service sous forme de méthodes crée une structure plus modulaire, qui facilite la maintenance et les évolutions de votre application.

  • Comme dans toute base de code, refactorisez pour éviter de répéter des blocs de code (le principe DRY : Don't Repeat Yourself).

  • Les literal types permettent de créer rapidement des types personnalisés, souvent utilisés pour limiter les choix pour un argument de méthode, par exemple : fileType: 'image' | 'video'

Maintenant que nous avons centralisé les interactions dans un service, nous pouvons ajouter du routing – rendez-vous au prochain chapitre !

Exemple de certificat de réussite
Exemple de certificat de réussite