Trabalhando com dados em Angular

Olá pessoal, meu nome é Sergey e sou desenvolvedor web. Perdoe-me Dmitry Karlovsky pela introdução emprestada, mas foram suas publicações que me inspiraram a escrever este artigo.


Hoje eu gostaria de falar sobre o trabalho com dados em aplicativos Angular em geral e modelos de domínio em particular.


Suponha que tenhamos uma lista de usuários que recebemos do servidor no formato


[ { "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" } ] 

e você precisa exibi-lo como na figura


Lista de usuários


Parece fácil - vamos tentar. Obviamente, para obter essa lista, teremos um serviço UserService seguinte. Observe que o link para o avatar do usuário não vem imediatamente na resposta, mas é formado com base no id do usuário.


 // 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`; } } 

O componente UserListComponent será responsável por exibir a lista de usuários.


 // 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) { } } 

E aqui já tínhamos um problema definido . Preste atenção na resposta do servidor. O campo last_name pode estar vazio e, se deixarmos o componente neste formulário, receberemos espaços indesejados antes da vírgula. Quais são as opções de solução?


  1. Você pode corrigir ligeiramente o modelo de exibição


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

    Mas, dessa maneira, sobrecarregamos o modelo com lógica, e ele se torna pouco legível, mesmo para uma tarefa tão simples. Mas o aplicativo ainda precisa crescer e crescer ...


  2. Puxe o código de um modelo para uma classe de componente adicionando um método de tipo


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

    Já é melhor, mas provavelmente o nome de usuário completo não será exibido em um local do aplicativo e teremos que duplicar esse código. Você pode levar esse método do componente para o serviço. Dessa forma, nos livraremos da possível duplicação de código, mas também não gosto muito dessa opção. E eu não gosto disso porque acontece que alguma entidade mais geral ( UserService ) deve saber sobre a estrutura da entidade User menor passada para ela. Não é o nível de responsabilidade dela, parece-me.



Na minha opinião, o problema surge principalmente do fato de tratarmos a resposta do servidor apenas como um conjunto de dados. Embora, de fato, seja uma lista de entidades da área de assunto de nosso aplicativo - uma lista de usuários. E se estamos falando de trabalhar com entidades, vale a pena usar as ferramentas mais adequadas para isso - os métodos de programação orientada a objetos.


Vamos começar criando a 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`; } } 

O construtor de classe é um desserializador de resposta do servidor. A lógica para determinar o nome completo do usuário naturalmente se transforma no método de um objeto da classe User , bem como na lógica para obter um avatar. Agora refazeremos o UserService para que ele nos retorne objetos da classe User como resultado do processamento da resposta do 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, o código do nosso componente se torna muito mais limpo e mais legível. Tudo o que pode ser chamado de lógica de negócios é encapsulado em modelos e é completamente reutilizável.


 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) { } } 

Vamos agora expandir os recursos do nosso modelo. Em teoria (nesse contexto, eu gosto da analogia com o padrão ActiveRecord ), os objetos de modelo de usuário devem ser responsáveis ​​não apenas pela obtenção de dados sobre si mesmos, mas também pela alteração deles. Por exemplo, podemos alterar a imagem do perfil do usuário. Como será o modelo do usuário expandido com essa funcionalidade?


 // 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); } } 

Parece bom, mas o modelo de User agora usa o serviço HttpClient e, em geral, ele pode se conectar e usar vários outros serviços - nesse caso, StorageService e AuthService (eles não são usados, mas adicionados apenas por exemplo). Acontece que, se quisermos usar o modelo de User em algum outro serviço ou componente, teremos que conectar todos os serviços associados a ele para criar objetos desse modelo. Parece muito inconveniente ... Você pode usar o serviço Injector (é claro, ele também precisará ser implementado, mas será garantido que seja apenas um) ou até mesmo criar uma entidade injetora externa que você não precisa implementar, mas vejo a maneira mais correta de delegar a criação de objetos de classe User no serviço UserService mesma maneira ele é responsável por obter a lista de usuários.


 // 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)))); } } 

Assim, movemos o método de criação do usuário para o UserService , que agora é mais apropriado para chamar a fábrica, e mudamos todo o trabalho de implementação de dependências para os ombros do Angular - só precisamos conectar o UserService no construtor.


No final, vamos remover a duplicação dos nomes dos métodos e introduzir convenções para os nomes das dependências injetadas. A versão final do serviço na minha visão deve ser assim.


 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)))); } } 

E propõe-se implementar UserFactory sob o nome 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) { } } 

Nesse caso, um objeto da classe UserFactory parece com uma classe User com métodos estáticos para obter uma lista de usuários e um método especial para criar novas entidades, e seus objetos contêm todos os métodos de lógica de negócios necessários associados a uma entidade específica.


Sobre isso, eu disse tudo o que queria. Estou ansioso para discutir nos comentários.


Update


Queria expressar minha gratidão a todos os que comentam. Você observou com razão que, para resolver o problema de exibir o nome, valeria a pena usar o Pipe . Concordo plenamente e fico surpreso por não ter tomado essa decisão. No entanto, o principal objetivo do artigo é mostrar um exemplo de criação de um modelo de domínio (neste caso, User ), que poderia encapsular convenientemente toda a lógica de negócios associada à sua essência. Paralelamente, tentei resolver o problema associado com a injeção de dependência.

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


All Articles