Episode 7 : Nous avons mis en place le style globale de notre application dans l’épisode précédent, et nous allons attaquer la partie plus métier de nos développements. En effet, dans notre application, nous ne voulons pas travailler avec de simple JSON venant du backend, nous voulons travailler avec des entités métiers : des utilisateurs, des tâches, etc. Nous pourrons également profiter de la puissance de TypeScript pour typer fortement nos méthodes et propriétés. Mais comment mettre en place des classes TypeScript capables de modéliser nos entités métiers, également appelées modèles ?

Nous avons déjà ajouté tous les éléments de style dont nous aurons besoin. Maintenant, nous allons attaquer les développements plus métier de notre application, en ajoutant des classes TypeScript capables de modéliser nos entités métiers : un Utilisateur, une Tâche, etc. Ces entités métiers sont également appelées des modèles.

Modéliser ses entités métiers avec TypeScript

Nous devons encore intégrer une chose, c’est la façon dont nous allons modéliser les entités métiers de notre application. En effet, nous allons passer notre temps à manipuler des utilisateurs, des journées de travail et des tâches. Autant modéliser ces objets tout de suite, cela nous servira de base pour la suite :

  • On disposera d’une représentation commune de nos entités dans l’ensemble du projet. 
  • On pourra appliquer un typage fort dans notre application : “Je veux que telle fonction retourne une Tâche, telle que je l’ai défini”.

Pour modéliser les entités métiers de notre application, nous allons utiliser des classes TypeScript. Là aussi, Angular CLI nous propose une commande pour générer ces classes dans notre projet : 

ng generate class shared/models/user 
ng generate class shared/models/workday 
ng generate class shared/models/task 

Nous générons trois nouvelles classes dans le sous-dossier models. Ce dossier contient nos entités métiers, et nous l’avons placé dans le même dossier que le SharedModule, car ces entités ont vocation à être partagées dans l’ensemble de notre application :

Nous devons maintenant ajouter des propriétés et des constructeurs pour chacune de nos entités.

L’entité « User »

 Commençons par modéliser notre entité la plus importante, notre utilisateur. Je commence par vous donner le code source de ce fichier, et je vous explique tout juste après. Dans le fichier user.ts, ajoutez le code suivant :

01 export class User { 
02   readonly id: string; // id de l’utilisateur 
03   email: string; // email de l’utilisateur 
04   isNewUser: boolean; // première connexion de l’utilisateur ? 
05   lastLoginDate: number; // date de la dernière connexion 
06   name: string; // nom de l’utilisateur 
07   avatar: string; // url de la photo de profil
08   pomodoroDuration: number; // durée des pomodoros 
09   registrationDate: number; // date d’inscription 
10   roles: Array<’USER’|’EMPLOYEE’>; // rôles
11
12   constructor(options: { 
13     id?: string, 
14     email?: string, 
15     isNewUser?: boolean, 
16     lastLoginDate?: number, 
17     name?: string, 
18     avatar?: string, 
19     pomodoroDuration?: number, 
20     registrationDate?: number, 
21     roles?: Array<’USER’|’EMPLOYEE’> 
22   } = {}) { 
23     this.id = options.id || ‘’; 
24     this.email = options.email || ‘’; 
25     this.isNewUser= options.isNewUser || true; 
26     this.lastLoginDate= options.lastLoginDate || 0;
27     this.name= options.name || ‘’;  
28     this.avatar = options.avatar || ‘’; 
29     this.pomodoroDuration = options.pomodoroDuration || 1500;
30     this.registrationDate= options.registrationDate || 0;  
31     this.roles = options.roles ||
32      this.email.endsWith('google.com')?  
33      ['USER', 'EMPLOYEE'] : ['USER'];
34   } 
35  } 

Il s’agit d’une classe assez simple, et pourtant j’imagine que vous avez déjà beaucoup de questions sur ce code. Je vous propose d’aborder d’abord les propriétés de cet objet, puis le constructeur. 

Les propriétés de l’entité User

On déclare les propriétés de l’entité User de la ligne 2 à la ligne 10. Il s’agit simplement d’une succession de nom de propriété avec un type associé. Il y a cependant plusieurs particularités : 

  • A la ligne 2, on utilise le mot-clé readonly. C’est un mot-clé TypeScript que vous pouvez appliquer sur une propriété d’un objet, pour vous assurer que cette propriété est en lecture seule et qu’elle ne peut pas être modifiée ailleurs dans votre application. Nous appliquons cette vérification sur l’identifiant de notre utilisateur, car l’identifiant doit être unique, et son unicité est garantie par notre base de données. Nous nous empêchons donc d’assigner un identifiant à un objet depuis notre application Angular : 
user.id = ‘12345’; // Typescript ne vas pas être content !  

Simple précaution, mais qui peut nous évider des erreurs d’inattention par la suite. 

  • A la ligne 10, on attribue le type Array<’USER’|’EMPLOYEE’> à la propriété rôles. Cela permet d’indiquer à TypeScript que les rôles sont un tableau qui ne peut contenir que des chaînes de caractères ‘USER’ et/ou ‘EMPLOYEE’.
  • A la ligne 31 et 32, on définit une valeur par défaut un peu particulière. Si l’utilisateur a une adresse email se terminant par “google.com”, on lui attribue le rôle “EMPLOYEE”. Sinon, on lui assigne les même droits qu’un utilisateur classique.             

J’ai une autre question… pourquoi la propriété ‘id est de type string ? 

En fait, cela dépend de votre base de données. Dans le cas d’une base de données MySQL traditionnelle, les identifiants des données stockées sont des nombres. Dans le cas du Firestore (base de données NoSQL hébergée dans le Cloud de Google), les identifiants uniques attribués aux objets sont générés sous forme de chaîne de caractères. C’est pour cela que l’on attribue le type string à nos identifiants. 

D’accord, et pourquoi les champs date sont des nombres ? 

Ah ! C’était un piège ! Je vous l’ai expliqué au début de ce cours.  

Rappelez-vous, dans le Firestore, nous allons sauvegarder nos dates sous forme de timestamp. Si vous ne vous souvenez plus ce qu’est un timestamp, relisez la partie concernant les données, au premier chapitre. C’est pour cela que les propriétés lastLoginDate et registrationDate contiendront des nombres. 

Maintenant, passons à la partie qui doit le plus vous interroger, il s’agit du constructeur de notre classe.

Le constructeur de l’entité « User »

On déclare le constructeur de notre classe User de la ligne 12 à 33. Cela peut vous sembler beaucoup de code pour un constructeur, qui a seulement pour rôle d’initialiser les propriétés de notre classe.  

En fait, la partie “spéciale” est le paramètre options du constructeur. Imaginons que je n’ajoute pas de typage à ce paramètre, le constructeur ressemblerai à ceci : 

constructor(options = {}) 

Le constructeur est en fait assez simple, on peut passer un paramètre facultatif, qui a comme valeur par défaut un objet vide. Mais ce qu’on voudrait maintenant, c’est demander à TypeScript de vérifier que l’on passe uniquement des valeurs attendues à notre constructeur. Par exemple, que ferions-nous avec le code suivant : 

let user = new User(4);  

Pas grand-chose… Le chiffre passé en paramètre est censé correspondre à quelle propriété de mon objet ? A la place, nous voudrions mettre en place un système qui nous permet de vérifier que seules des variables avec des noms correspondant aux propriétés de mon objet puissent être passé en paramètre du constructeur. Pour cela, on ajoute simplement un typage au paramètre options 

01 options: { 
02   id?: string, 
03   email?: string, 
04   isNewUser?: boolean, 
05   lastLoginDate?: number, 
06   name?: string, 
07   avatar?: string, 
08   pomodoroDuration?: number, 
09   registrationDate?: number, 
10   roles?: Array<’USER’|’EMPLOYEE’> 
11 } 

Ainsi, nous indiquons à TypeScript que dans notre constructeur nous voulons un objet contenant une liste de propriétés facultatives (grâce à l’opérateur “?”) avec des noms identiques aux propriétés de mon objet. Si une valeur n’est pas définie en paramètre du constructeur, alors c’est la valeur par défaut qui est attribué, grâce à l’opérateur OU :  

01 // La chaîne vide est la valeur par défaut de “id" :   
02 this.id = options.id || ‘’;  
03
04 // Identique pour la propriété “Email” : 
05 this.email = options.email || ‘’; 
06
07 // Un utilisateur est défini comme nouveau par défaut : 
08 this.isNewUser= options.isNewUser || true; 
09
10 // etc... 

Le typage du paramètre du constructeur, couplé avec les valeurs par défaut, nous permet une plus grande flexibilité pour instancier nos utilisateurs :

01 // Créer un utilisateur avec un email personnalisé, 
02 // et les autres propriétés avec une valeur par défaut : 
03 let customEmailUser = 
04  new User({ email: ‘john.doe@awesome.com’ });
05 
06 // affiche ‘john.doe@awesome.com’ 
07 console.info(customEmailUser.email);
08 
09 // Créer un utilisateur avec des valeurs par défaut :
10 let defaultUser = New User(); 
11 console.info(defaultUser.email); // affiche ‘’.  

Maintenant, nous avons en place une solution robuste pour instancier les utilisateurs de notre application. 

J’espère ne pas vous avoir trop ennuyé avec toutes ces explications, mais d’après moi il était nécessaire de ne pas laisser de mystères s’installer dès le début du projet. Je vous rassure, les modèles fonctionnent tous de la même manière. 

L’entité « Task »

Nous implémentons maintenant notre entité Task, en appliquant les mêmes principes que nous avons abordés précédemment :

01 export class Task { 
02  // nombre maximum de pomodoros
03  static readonly pomodoroLimit: number = 5; 
04  completed: boolean; // tâche terminé ou non 
05  done: number; // nombre de pomodoros effectués
06  title: string; // intitulé de la tâche
07  todo: number; // nombre de pomodoros prévus 
08
09  constructor(options: { 
10   completed?: boolean,
11   done?: number, 
12   title?: string,
13   todo?: number 
14  } = {}) { 
15   this.completed = options.completed || false; 
16   this.done = options.done || 0; 
17   this.title = options.title || ‘’; 
18   this.todo = options.todo || 1; 
19  }
20 }

Là aussi, la classe est très simple, mais il y a une petite particularité concernant la propriété pomodoroLimit, à la ligne 3. J’utilise le mot-clé readonly que je viens de vous présenter, avec le mot-clé static, qui permet de déclarer une propriété statique pour notre objet. Pour rappel, il s’agit d’un terme propre à la programmation par objet, qui permet de définir une propriété rattachée à la classe Task, et non aux instances de cette classe. Concrètement, cela signifie que la propriété pomodoroLimit que j’ai défini n’est pas spécifique à une tâche, mais concerne toutes les tâches. En effet, le nombre maximum de pomodoros possibles par tâche est limité à cinq, quelle que soit la tâche. Ensuite, on pourra écrire quelque chose comme ça : 

console.log(Task.pomodoroLimit); // affiche ‘5’ 

Dans l’exemple de code ci-dessus, Task désigne bien notre classe, et non une instance. Retenez simplement que vous pouvez définir des propriétés communes à toutes les instances d’une classe, grâce aux propriétés statiques. 

Pourquoi il n’y a pas d’identifiant pour la classe Task ? 

Comme nous le verrons juste après, nous allons rattacher nos tâches à une autre entité que sont les journées de travail (Workdays). Nous ne sauvegarderons jamais une tâche en tant que telle, car nous passerons forcément par une journée de travail pour y accéder. Nous n’avons donc pas besoin de lui attribuer un identifiant. 

L’entité « Workday »

Maintenant, nous pouvons passer à notre dernière entité. La particularité de cette entité, c’est qu’elle fait le lien entre nos utilisateurs et leurs tâches. Nous allons voir comment modéliser ces relations dans notre classe.  

L’entité modélisant une journée de travail, accessible dans le fichier workday.ts, ressemble à ceci : 

01 import { Task } from './task; 
02
03 export class Workday { 
04   readonly id: string; // identifiant de la journée de travail
05   dueDate: number; // date de la journée de travail
06   notes?: string; // notes éventuelles (facultatif)
07   tasks: Task[]; // la liste des tâches à faire 
08   userId: string; // identifiant de l’utilisateur 
09
10   constructor(options: { 
11     id?: string, 
12     dueDate?: number, 
13     notes?: string, 
14     tasks?: Task[], 
15     userId: string  
16   }){  
17     this.id = options.id || null; 
18     this.dueDate = options.dueDate || 0; 
19     this.notes = options.notes || ‘’; 
20     this.tasks = [new Task()]; 
21     this.userId = options.userId; 
22  }
23 }

La première chose que nous faisons ici, c’est de lier l’entité journée de travail avec l’entité tâche. Nous importons donc la classe Task dès la première ligne. Puis nous définissons le type de la propriété tasks à la ligne 7, qui est un tableau d’instances de Task. En effet, nous avons défini une classe pour modéliser les tâches au sein de notre application, et nous pouvons l’utiliser pour typer les propriétés d’autres classes ! 

Le code est ainsi robuste, car on s’assure qu’une journée de travail contient bien les tâches comme nous les avons définis, et en plus notre code est lisible. 

Le deuxième point concerne la liaison entre l’entité journée de travail et l’entité utilisateur. C’est ce que nous faisons à la ligne 8 en déclarant la propriété userId, qui contiendra l’identifiant de l’utilisateur qui doit réaliser cette journée de travail. Le plus important est à la ligne 15, où vous pourrez remarquer qu’il n’y a pas l’opérateur “?” devant la propriété userId passé en paramètre, ni de valeur par défaut pour le paramètre de notre constructeur. Concrètement, cela signifie qu’il est impossible de créer une nouvelle journée de travail dans notre application, sans lui associer un utilisateur : 

01 // erreur : nous devons associer un utilisateur... 
02 let workday = new Workday(); 
03
04 // ok, c’est bon ! 
05 let workday = new Workday({userId : ‘johnDoe’}); 

Il s’agit d’une petite précaution que nous mettons en place, pour garantir la consistance de nos données. On est ainsi certain de ne pas tomber sur une journée de travail orpheline dans notre application, sans savoir à qui elle est destinée.  

D’accord, j’ai compris. Mais pourquoi nous avons défini des classes pour nos entités, et non des interfaces ? 

Si vous allez faire un tour sur internet, il est courant de trouver des exemples de code qui implémente des interfaces TypeScript pour les entités, plutôt que des classes : 

01 export interface User {
02  firstname: string;  
03  lastname: string; 
04 } 

Les classes et les interfaces permettent toutes les deux de mettre en place la vérification de type (type-checking) auprès de TypeScript. Cependant, il existe une différence notable entre les deux. 

 Une classe est une sorte de moule, à partir duquel nous pouvons créer des objets partageant les mêmes propriétés et méthodes. Au contraire, une interface permet seulement de décrire un objet, mais ne permet pas de l’instancier. 

Dans notre cas, nous souhaitons instancier de nouveaux utilisateurs et journées de travail à partir des données du serveur. Nous utilisons donc des classes plutôt que des interfaces. 

Aussi, sauf erreur de ma part, nous n’avons pas ajouté nos entités dans le SharedModule, même si nous les avons placés dans le dossier shared… c’est un oubli ou c’est volontaire ? 

En fait, nos classes d’entités TypeScript ne sont pas des éléments propres à Angular. Il ne s’agit ni de composants, ni de services, etc. Nous importerons donc les classes d’entités à chaque fois que nous aurons besoin de typer un paramètre, ou un retour de fonction. Par contre, nous plaçons quand même nos classes dans le dossier shared, car ce sont tout de même des éléments qui sont partagés dans toute l’application. 

Nous savons maintenant modéliser nos entités métiers au sein de classes TypeScript. Cet épisode nous a permis de nous concentrer sur la partie plus métier de notre code, et même si pour l’utilisateur il n’y a rien de visible, notre code sera beaucoup plus robuste, et plus sympathique à (re)lire !

Dans le prochain épisode, nous allons avancer sérieusement sur nos développements, puisque nous verrons comment organiser l’arborescence de nos composants. Combien créer de composants, à quel point faut-il découper, ou non ? Comment organiser tous ces nouveaux composants, pour que notre projet reste « propre » ? Nous verrons la réponse avec l’architecture de composants la plus populaire nommée « Smart & Dumb components ». A très vite !


Si vous n’avez pas la patience de chercher les articles sur le blog, je peux vous les envoyer dans l’ordre, directement dans votre boîte mail. En fait, cet article fait partie d’une série de 10 articles extraits de l’ouvrage Maîtriser Angular pour l’entreprise.