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 fonctionfind()
;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 :
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 !