- PVSM.RU - https://www.pvsm.ru -
Всем привет, меня зовут Сергей и я web разработчик. Да простит меня Дмитрий Карловский [1] за заимствованное вступление, но именно его публикации [2] вдохновили меня написание этой статьи.
Сегодня хотелось бы поговорить о работе с данными в Angular [3] приложениях в целом и о моделях предметной области в частности.
Предположим, что у нас есть некий список пользователей, который мы получаем с сервера в виде
[
{
"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"
}
]
а отобразить его нужно как на картинке
Выглядит несложно — давайте попробуем. Разумеется для получения этого списка у нас будет сервис UserService
примерно следующего вида. Обратите внимание, что ссылка на аватарку пользователя не приходит сразу в ответе, а формируется на основе id
пользователя.
// 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`;
}
}
За отображения списка пользователей будет отвечать компонент UserListComponent
.
// 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) { }
}
И вот тут у нас уже наметилась определенная проблема. Обратите внимание на ответ сервера. Поле last_name
может быть пустым и если мы оставим компонент в таком виде, то будем получать нежелательные пробелы перед запятой. Какие есть варианты решения?
Можно немного поправить шаблон отображения
<p>
<b>{{[user.first_name, user.last_name].filter(el => !!el).join(' ')}}</b>,
{{user.position}}
</p>
Но таким образом мы перегружаем шаблон логикой, и он становится плохочитаемым даже для такой простой задачи. А ведь приложению еще расти и расти...
Вынести код из шаблона в класс компоненты, добавив метод типа
getUserFullName(user: UserServerResponse): string {
return [user.first_name, user.last_name].filter(el => !!el).join(' ');
}
Уже получше, но скорее всего полное имя пользователя будет отображаться не в одном месте приложения, и нам придется дублировать этот код. Можно вынести этот метод из компоненты в сервис. Таким образом мы избавимся от возможного дублирования кода, но такой вариант мне тоже не очень нравится. А не нравится потому, что получается, что некоторая более общая сущность (UserService
) должна знать о структуре передаваемой в нее более мелкой сущности User
. Не ее уровень ответственности, как мне кажется.
На мой взгляд проблема в первую очередь возникает из-за того, что мы относимся к ответу сервера исключительно как к набору данных. Хотя ведь на самом деле он представляет собой список сущностей из предметной области нашего приложения — список пользователей. А если мы говорим о работе с сущностями, то стоит применять наиболее подходящий для этого инструментарий — методы объектно-ориентированного программирования.
Начнем с того, что создадим класс 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`;
}
}
Конструктор класса представляет собой десериализатор ответа сервера. Логика определения полного имени пользователя естественным образом превращается в метод объекта класса User
равно как и логика получения аватарки. Теперь переделаем UserService
так, чтоб он возвращал нам объекты класса User
как результат обработки ответа сервера
// 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))));
}
}
В результате код нашей компоненты становится значительно более чистым и читабельным. Все то, что можно назвать бизнес-логикой, инкапсулировано в моделях и является полностью переиспользуемым.
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) {
}
}
Давайте теперь расширим возможности нашей модели. По идее (в данном контексте мне нравится аналогия с паттерном ActiveRecord
[4]) объекты модели пользователя должны быть ответственны не только за получение данных о себе, но и за их изменение. Например, у нас может быть возможность сменить аватарку пользователя. Как будет выглядеть расширенная такой функциональностью модель пользователя?
// 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);
}
}
Выглядит неплохо, но модель User
теперь использует сервис HttpClient
и, вообще говоря, она вполне может подключать и использовать различные другие сервисы — в данном случае это StorageService
и AuthService
(они не используются, а добавлены просто для примера). Получается, что если мы захотим в каком-нибудь другом сервисе или компоненте использовать модель User
, нам для создания объектов этой модели придется подключать все связанные с нею сервисы. Выглядит весьма неудобно… Можно воспользоваться сервисом Injector
(его конечно тоже придется внедрять, но он гарантированно будет только один) или вообще создать внешнюю сущность инжектора [5] которую внедрять не придется, но более правильным мне видится делегирования метода создания объектов класса User
сервису UserService
аналогично тому, как он отвечает за получения списка пользователей.
// 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))));
}
}
Таким образом мы переместили метод создания пользователя в UserService
, который уместнее теперь называть фабрикой, и переложили всю работу по внедрению зависимостей на плечи Ангуляра — нам необходимо только подключить 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 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))));
}
}
А в компонент UserFactory
предлагается внедрять под именем 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, private http: HttpClient) {
http.get('/users').subscribe(res => console.log(res));
}
}
В этом случае объект класса UserFactory
внешне выглятит как класс User
со статическими методами для получения списка пользователей и специальным методом создания новых сущностей, а его объекты содержат все необходимые методы бизнес-логики, связанные с конкретной сущностью.
На этом я рассказал все, что хотел. С нетерпением буду ждать обсуждения в комментариях.
Автор: SergeyMell
Источник [6]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/javascript/287413
Ссылки в тексте:
[1] Дмитрий Карловский: https://habr.com/users/vintage/
[2] публикации: https://habr.com/users/vintage/posts/
[3] Angular: https://angular.io/
[4] ActiveRecord
: https://ru.wikipedia.org/wiki/ActiveRecord
[5] внешнюю сущность инжектора: https://stackoverflow.com/questions/49507928/how-to-inject-httpclient-in-static-method-or-custom-class
[6] Источник: https://habr.com/post/418463/?utm_campaign=418463
Нажмите здесь для печати.