Hola a todos, mi nombre es Sergey y soy desarrollador web. Perd贸name Dmitry Karlovsky por la introducci贸n prestada, pero fueron sus publicaciones las que me inspiraron a escribir este art铆culo.
Hoy me gustar铆a hablar sobre trabajar con datos en aplicaciones angulares en general y modelos de dominio en particular.
Supongamos que tenemos una lista de usuarios que recibimos del servidor en el formulario
[ { "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" } ]
y necesitas mostrarlo como en la imagen

Parece f谩cil, intent茅moslo. Por supuesto, para obtener esta lista, tendremos un servicio de UserService
siguiente. Tenga en cuenta que el enlace al avatar del usuario no aparece inmediatamente en la respuesta, sino que se forma en funci贸n de la id
usuario.
// 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`; } }
El componente UserListComponent
ser谩 responsable de mostrar la lista de usuarios.
// 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) { } }
Y aqu铆 ya ten铆amos un problema definido . Presta atenci贸n a la respuesta del servidor. El campo last_name
puede estar vac铆o y si dejamos el componente en este formulario, recibiremos espacios no deseados antes de la coma. 驴Cu谩les son las opciones de soluci贸n?
Puede corregir ligeramente la plantilla de visualizaci贸n
<p> <b>{{[user.first_name, user.last_name].filter(el => !!el).join(' ')}}</b>, {{user.position}} </p>
Pero de esta manera, sobrecargamos la plantilla con l贸gica, y se vuelve poco legible incluso para una tarea tan simple. Pero la aplicaci贸n todav铆a tiene que crecer y crecer ...
Extraiga el c贸digo de una plantilla a una clase de componente agregando un m茅todo de tipo
getUserFullName(user: UserServerResponse): string { return [user.first_name, user.last_name].filter(el => !!el).join(' '); }
Ya es mejor, pero lo m谩s probable es que el nombre de usuario completo no se muestre en un lugar de la aplicaci贸n, y tendremos que duplicar este c贸digo. Puede llevar este m茅todo del componente al servicio. De esta manera, eliminaremos la posible duplicaci贸n de c贸digo, pero tampoco me gusta esta opci贸n. Y no me gusta porque resulta que alguna entidad m谩s general ( UserService
) deber铆a saber sobre la estructura de la entidad de User
m谩s peque帽a que se le pasa. No es su nivel de responsabilidad, me parece.
En mi opini贸n, el problema surge principalmente del hecho de que tratamos la respuesta del servidor 煤nicamente como un conjunto de datos. Aunque, de hecho, es una lista de entidades del 谩rea tem谩tica de nuestra aplicaci贸n, una lista de usuarios. Y si estamos hablando de trabajar con entidades, entonces vale la pena usar las herramientas m谩s adecuadas para esto: los m茅todos de programaci贸n orientada a objetos.
Comencemos creando la clase Usuario
// 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`; } }
El constructor de la clase es un deserializador de respuesta del servidor. La l贸gica para determinar el nombre de usuario completo se convierte naturalmente en un m茅todo de un objeto de la clase User
, as铆 como la l贸gica para obtener un avatar. Ahora UserService
el UserService
para que nos devuelva objetos de la clase User
como resultado del procesamiento de la respuesta del servidor
// 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)))); } }
Como resultado, el c贸digo de nuestro componente se vuelve mucho m谩s limpio y m谩s legible. Todo lo que se puede llamar l贸gica de negocios est谩 encapsulado en modelos y es completamente reutilizable.
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) { } }
Expandamos ahora las capacidades de nuestro modelo. En teor铆a (en este contexto, me gusta la analog铆a con el patr贸n ActiveRecord
), los objetos del modelo de usuario deben ser responsables no solo de obtener datos sobre s铆 mismos, sino tambi茅n de cambiarlos. Por ejemplo, podemos cambiar la imagen de perfil del usuario. 驴C贸mo se ver谩 el modelo de usuario ampliado con tal funcionalidad?
// 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); } }
Se ve bien, pero el modelo de User
ahora usa el servicio HttpClient
y, en t茅rminos generales, puede conectarse y usar varios otros servicios, en este caso, AuthService
y AuthService
(no se usan, sino que se agregan solo, por ejemplo). Resulta que si queremos usar el modelo de User
en alg煤n otro servicio o componente, tendremos que conectar todos los servicios asociados con 茅l para crear objetos de este modelo. Parece muy inconveniente ... Puede usar el servicio de Injector
(por supuesto, tambi茅n tendr谩 que implementarse, pero se garantizar谩 que sea solo uno) o incluso crear una entidad de inyector externo que no tenga que implementar, pero veo la forma m谩s correcta de delegar la creaci贸n de objetos de clase de User
al servicio UserService
servicio de UserService
misma manera 茅l es responsable de obtener la lista de usuarios.
// 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)))); } }
Por lo tanto, trasladamos el m茅todo de creaci贸n de usuarios a UserService
, que ahora es m谩s apropiado para llamar a la f谩brica, y cambiamos todo el trabajo de implementar dependencias a los hombros de Angular: solo necesitamos conectar el UserService
en el constructor.
Al final, eliminemos la duplicaci贸n de los nombres de los m茅todos e introduzcamos convenciones para los nombres de las dependencias inyectadas. La versi贸n final del servicio en mi visi贸n deber铆a verse as铆.
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)))); } }
Y se propone implementar UserFactory
bajo el nombre de 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) { } }
En este caso, un objeto de la clase UserFactory
parece a una clase User
con m茅todos est谩ticos para obtener una lista de usuarios y un m茅todo especial para crear nuevas entidades, y sus objetos contienen todos los m茅todos de l贸gica de negocios necesarios asociados con una entidad en particular.
Sobre esto, le dije todo lo que quer铆a. Espero discutir en los comentarios.
Actualizaci贸n
Quer铆a expresar mi gratitud a todos los que comentan. Usted not贸 correctamente que para resolver el problema de mostrar el nombre, valdr铆a la pena usar Pipe
. Estoy completamente de acuerdo y me sorprende que no haya tomado esta decisi贸n. Sin embargo, el objetivo principal del art铆culo es mostrar un ejemplo de creaci贸n de un modelo de dominio (en este caso, User
), que podr铆a encapsular convenientemente toda la l贸gica de negocios asociada con su esencia. Paralelamente, trat茅 de resolver el problema que lo acompa帽a con la inyecci贸n de dependencia.