Travailler avec des données dans Angular

Bonjour à tous, je m'appelle Sergey et je suis développeur web. Pardonnez-moi Dmitry Karlovsky pour l'introduction empruntée, mais ce sont ses publications qui m'ont inspiré pour écrire cet article.


Aujourd'hui, je voudrais parler du travail avec les données dans les applications angulaires en général et les modèles de domaine en particulier.


Supposons que nous ayons une liste d'utilisateurs que nous recevons du serveur sous la forme


[ { "id": 1, "first_name": "James", "last_name": "Hetfield", "position": "Web developer" }, { "id": 2, "first_name": "Elvis", "last_name": "", "position": "Project manager" }, { "id": 3, "first_name": "Steve", "last_name": "Vai", "position": "QA engineer" } ] 

et vous devez l'afficher comme sur l'image


Liste d'utilisateurs


Cela semble facile - essayons. Bien sûr, pour obtenir cette liste, nous aurons un service UserService suivant. Veuillez noter que le lien vers l'avatar de l'utilisateur ne vient pas immédiatement dans la réponse, mais est formé en fonction de l' id utilisateur.


 // UserService import {Injectable} from '@angular/core'; import {HttpClient} from '@angular/common/http'; import {Observable} from 'rxjs'; import {UserServerResponse} from './user-server-response.interface'; @Injectable() export class UserService { constructor(private http: HttpClient) { } getUsers(): Observable<UserServerResponse[]> { return this.http.get<UserServerResponse[]>('/users'); } getUserAvatar(userId: number): string { return `/users/${userId}/avatar`; } } 

Le composant UserListComponent sera responsable de l'affichage de la liste des utilisateurs.


 // UserListComponent import {Component} from '@angular/core'; import {UserService} from '../services/user.service'; @Component({ selector: 'app-user-list', template: ` <div *ngFor="let user of users | async"> <img [src]="userService.getUserAvatar(user.id)"> <p><b>{{user.first_name}} {{user.last_name}}</b>, {{user.position}}</p> </div> ` }) export class UserListComponent { users = this.userService.getUsers(); constructor(public userService: UserService) { } } 

Et ici, nous avions déjà un problème certain . Faites attention à la réponse du serveur. Le champ last_name peut être vide et si nous laissons le composant sous cette forme, nous recevrons des espaces indésirables avant la virgule. Quelles sont les options de solution?


  1. Vous pouvez légèrement corriger le modèle d'affichage


     <p> <b>{{[user.first_name, user.last_name].filter(el => !!el).join(' ')}}</b>, {{user.position}} </p> 

    Mais de cette façon, nous surchargeons le modèle avec la logique, et il devient mal lisible, même pour une tâche aussi simple. Mais l'application doit encore grandir et grandir ...


  2. Extraire le code d'un modèle dans une classe de composants en ajoutant une méthode de type


     getUserFullName(user: UserServerResponse): string { return [user.first_name, user.last_name].filter(el => !!el).join(' '); } 

    Déjà mieux, mais le nom d'utilisateur complet ne sera probablement pas affiché à un seul endroit de l'application, et nous devrons dupliquer ce code. Vous pouvez transférer cette méthode du composant au service. De cette façon, nous nous débarrasserons de la duplication de code possible, mais je n'aime pas vraiment non plus cette option. Et je ne l'aime pas car il s'avère qu'une entité plus générale ( UserService ) devrait connaître la structure de la plus petite entité User qui lui est passée. Pas son niveau de responsabilité, il me semble.



À mon avis, le problème vient principalement du fait que nous traitons la réponse du serveur uniquement comme un ensemble de données. Bien qu'il s'agisse en fait d'une liste d'entités du domaine de notre application - une liste d'utilisateurs. Et si nous parlons de travailler avec des entités, cela vaut la peine d'utiliser les outils les plus appropriés pour cela - les méthodes de programmation orientée objet.


Commençons par créer la classe User


 // User export class User { readonly id; readonly firstName; readonly lastName; readonly position; constructor(userData: UserServerResponse) { this.id = userData.id; this.firstName = userData.first_name; this.lastName = userData.last_name; this.position = userData.position; } fullName(): string { return [this.firstName, this.lastName].filter(el => !!el).join(' '); } avatar(): string { return `/users/${this.id}/avatar`; } } 

Le constructeur de classe est un désérialiseur de réponse de serveur. La logique de détermination du nom d'utilisateur complet se transforme naturellement en une méthode d'un objet de la classe User , ainsi que la logique d'obtention d'un avatar. Maintenant, nous allons refaire le UserService afin qu'il nous renvoie des objets de la classe User suite au traitement de la réponse du serveur


 // UserService import {Injectable} from '@angular/core'; import {HttpClient} from '@angular/common/http'; import {map} from 'rxjs/operators'; import {UserServerResponse} from './user-server-response.interface'; import {User} from './user.model'; @Injectable() export class UserService { constructor(private http: HttpClient) { } getUsers(): Observable<User[]> { return this.http.get<UserServerResponse[]>('/users') .pipe(map(listOfUsers => listOfUsers.map(singleUser => new User(singleUser)))); } } 

En conséquence, le code de notre composant devient beaucoup plus propre et plus lisible. Tout ce que l'on peut appeler la logique métier est encapsulé dans des modèles et est complètement réutilisable.


 import {Component} from '@angular/core'; import {UserService} from '../services/user.service'; @Component({ selector: 'app-user-list', template: ` <div *ngFor="let user of users | async"> <img [src]="user.avatar()"> <p><b>{{user.fullName()}}</b>, {{user.position}}</p> </div> ` }) export class UserListComponent { users = this.userService.getUsers(); constructor(private userService: UserService) { } } 

Étendons maintenant les capacités de notre modèle. En théorie (dans ce contexte, j'aime l'analogie avec le modèle ActiveRecord ), les objets du modèle utilisateur devraient être responsables non seulement d'obtenir des données les concernant, mais également de les modifier. Par exemple, nous pouvons être en mesure de modifier la photo de profil de l'utilisateur. À quoi ressemblera le modèle utilisateur étendu avec une telle fonctionnalité?


 // User export class User { // ... constructor(userData: UserServerResponse, private http: HttpClient, private storage: StorageService, private auth: AuthService) { // ... } // ... updateAvatar(file: Blob) { const data = new FormData(); data.append('avatar', file); return this.http.put(`/users/${this.id}/avatar`, data); } } 

Cela semble bon, mais le modèle User utilise maintenant le service HttpClient et, d'une manière générale, il peut bien se connecter et utiliser divers autres services - dans ce cas, AuthService et AuthService (ils ne sont pas utilisés, mais ajoutés juste par exemple). Il s'avère que si nous voulons utiliser le modèle User dans un autre service ou composant, nous devrons connecter tous les services qui lui sont associés pour créer des objets de ce modèle. Cela semble très gênant ... Vous pouvez utiliser le service Injector (bien sûr, il devra également être implémenté, mais il sera garanti qu'il n'en sera qu'un) ou même créer une entité injecteur externe que vous n'avez pas à implémenter, mais je vois la manière la plus correcte de déléguer la création d'objets de classe User au service UserService même manière il est chargé d'obtenir la liste des utilisateurs.


 // UserService import {Injectable} from '@angular/core'; import {HttpClient} from '@angular/common/http'; import {Observable} from 'rxjs'; import {map} from 'rxjs/operators'; import {UserServerResponse} from './user-server-response.interface'; import {User} from './user.model'; @Injectable() export class UserService { constructor(private http: HttpClient, private storage: StorageService, private auth: AuthService) { } createUser(userData: UserServerResponse) { return new User(userData, this.http, this.storage, this.auth); } getUsers(): Observable<User[]> { return this.http.get<UserServerResponse[]>('/users') .pipe(map(listOfUsers => listOfUsers.map(singleUser => this.createUser(singleUser)))); } } 

Ainsi, nous avons déplacé la méthode de création d'utilisateur vers UserService , qui est maintenant plus approprié pour appeler l'usine, et déplacé tout le travail d'implémentation des dépendances sur les épaules d'Angular - nous avons seulement besoin de connecter UserService dans le constructeur.


Au final, supprimons la duplication des noms des méthodes et introduisons les conventions pour les noms des dépendances injectées. La version finale du service dans ma vision devrait ressembler à ceci.


 import {Injectable} from '@angular/core'; import {HttpClient} from '@angular/common/http'; import {Observable} from 'rxjs'; import {map} from 'rxjs/operators'; import {UserServerResponse} from './user-server-response.interface'; import {User} from './user.model'; @Injectable() export class UserFactory { constructor(private http: HttpClient, private storage: StorageService, private auth: AuthService) { } create(userData: UserServerResponse) { return new User(userData, this.http, this.storage, this.auth); } list(): Observable<User[]> { return this.http.get<UserServerResponse[]>('/users') .pipe(map(listOfUsers => listOfUsers.map(singleUser => this.create(singleUser)))); } } 

Et il est proposé d'implémenter UserFactory sous le nom User


 import { Component } from '@angular/core'; import {HttpClient} from '@angular/common/http'; import {UserFactory} from './services/user.service'; @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.css'] }) export class AppComponent { title = 'app'; users = this.User.list(); constructor(private User: UserFactory) { } } 

Dans ce cas, un objet de la classe UserFactory ressemble à une classe User avec des méthodes statiques pour obtenir une liste d'utilisateurs et une méthode spéciale pour créer de nouvelles entités, et ses objets contiennent toutes les méthodes de logique métier nécessaires associées à une entité particulière.


Là-dessus, j'ai dit tout ce que je voulais. J'ai hâte de discuter dans les commentaires.


Mettre à jour


Je voulais exprimer ma gratitude à tous ceux qui ont commenté. Vous avez noté à juste titre que pour résoudre le problème d'affichage du nom, il serait utile d'utiliser Pipe . Je suis tout à fait d'accord et je m'étonne moi-même pourquoi je n'ai pas pris cette décision. Néanmoins, l'objectif principal de l'article est de montrer un exemple de création d'un modèle de domaine (dans ce cas, l' User ), qui pourrait facilement encapsuler toute la logique métier associée à son essence. En parallèle, j'ai essayé de résoudre le problème d'accompagnement avec l'injection de dépendance.

Source: https://habr.com/ru/post/fr418463/


All Articles