Halo semuanya, nama saya Sergey dan saya seorang pengembang web. Maafkan saya, Dmitry Karlovsky atas perkenalan yang dipinjam, tetapi terbitannya yang menginspirasi saya untuk menulis artikel ini.
Hari ini saya ingin berbicara tentang bekerja dengan data dalam aplikasi Angular pada umumnya dan model domain pada khususnya.
Misalkan kita memiliki daftar pengguna yang kami terima dari server dalam formulir
[ { "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" } ]
dan Anda perlu menampilkannya seperti pada gambar

Itu terlihat mudah - mari kita coba. Tentu saja, untuk mendapatkan daftar ini, kami akan memiliki layanan UserService
berikut ini. Harap perhatikan bahwa tautan ke avatar pengguna tidak langsung muncul di respons, tetapi dibentuk berdasarkan id
pengguna.
// 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`; } }
Komponen UserListComponent
akan bertanggung jawab untuk menampilkan daftar pengguna.
// 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) { } }
Dan di sini kita sudah memiliki masalah yang pasti . Perhatikan respons server. Bidang last_name
mungkin kosong dan jika kita meninggalkan komponen dalam formulir ini, kita akan menerima spasi yang tidak diinginkan sebelum koma. Apa saja opsi solusinya?
Anda dapat sedikit memperbaiki template tampilan
<p> <b>{{[user.first_name, user.last_name].filter(el => !!el).join(' ')}}</b>, {{user.position}} </p>
Tetapi dengan cara ini, kita membebani template dengan logika, dan itu menjadi buruk dibaca bahkan untuk tugas yang begitu sederhana. Tetapi aplikasi ini masih harus tumbuh dan berkembang ...
Tarik kode dari templat ke kelas komponen dengan menambahkan metode tipe
getUserFullName(user: UserServerResponse): string { return [user.first_name, user.last_name].filter(el => !!el).join(' '); }
Sudah lebih baik, tetapi kemungkinan besar nama pengguna lengkap tidak akan ditampilkan di satu tempat aplikasi, dan kami harus menduplikasi kode ini. Anda dapat menggunakan metode ini dari komponen ke layanan. Dengan cara ini kita akan menyingkirkan kemungkinan duplikasi kode, tetapi saya juga tidak terlalu menyukai opsi ini. Dan saya tidak suka karena ternyata beberapa entitas yang lebih umum ( UserService
) harus tahu tentang struktur entitas User
lebih kecil masuk ke dalamnya. Bukan tingkat tanggung jawabnya, menurut saya.
Menurut pendapat saya, masalah utamanya muncul dari kenyataan bahwa kami memperlakukan respons server hanya sebagai dataset. Meskipun, pada kenyataannya, ini adalah daftar entitas dari area subjek aplikasi kita - daftar pengguna. Dan jika kita berbicara tentang bekerja dengan entitas, maka ada baiknya menggunakan alat yang paling cocok untuk ini - metode pemrograman berorientasi objek.
Mari kita mulai dengan membuat kelas Pengguna
// 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`; } }
Konstruktor kelas adalah deserializer respons server. Logika untuk menentukan nama pengguna lengkap secara alami berubah menjadi metode objek kelas User
, serta logika untuk mendapatkan avatar. Sekarang kita akan membuat ulang UserService
sehingga mengembalikan kita objek kelas User
sebagai hasil dari pemrosesan respons server
// 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)))); } }
Akibatnya, kode komponen kami menjadi jauh lebih bersih dan lebih mudah dibaca. Segala sesuatu yang dapat disebut logika bisnis diringkas dalam model dan sepenuhnya dapat digunakan kembali.
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) { } }
Sekarang mari kita memperluas kemampuan model kita. Dalam teori (dalam konteks ini, saya suka analogi dengan pola ActiveRecord
), objek model pengguna harus bertanggung jawab tidak hanya untuk mendapatkan data tentang diri mereka sendiri, tetapi juga untuk mengubahnya. Misalnya, kami mungkin dapat mengubah gambar profil pengguna. Seperti apa model pengguna yang akan diperluas dengan fungsi seperti itu?
// 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); } }
Ini terlihat bagus, tetapi model User
sekarang menggunakan layanan HttpClient
dan, secara umum, ia dapat terhubung dan menggunakan berbagai layanan lainnya - dalam hal ini, StorageService
dan AuthService
(mereka tidak digunakan, tetapi ditambahkan hanya sebagai contoh). Ternyata jika kita ingin menggunakan model User
di beberapa layanan atau komponen lain, kita harus menghubungkan semua layanan yang terkait dengannya untuk membuat objek model ini. Itu terlihat sangat merepotkan ... Anda dapat menggunakan layanan Injector
(tentu saja, itu juga harus diimplementasikan, tetapi akan dijamin hanya satu) atau bahkan membuat entitas injektor eksternal yang tidak harus Anda implementasikan, tetapi saya melihat cara yang lebih tepat untuk mendelegasikan pembuatan objek kelas User
ke layanan UserService
dengan cara yang sama dia bertanggung jawab untuk mendapatkan daftar pengguna.
// 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)))); } }
Jadi, kami memindahkan metode pembuatan pengguna ke UserService
, yang sekarang lebih tepat untuk memanggil pabrik, dan mengalihkan semua pekerjaan penerapan dependensi ke bahu Angular - kita hanya perlu menghubungkan UserService
di konstruktor.
Pada akhirnya, mari kita hapus duplikasi dari nama-nama metode dan memperkenalkan konvensi untuk nama-nama dependensi yang disuntikkan. Versi terakhir dari layanan dalam visi saya akan terlihat seperti ini.
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)))); } }
Dan diusulkan untuk mengimplementasikan UserFactory
dengan nama 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) { } }
Dalam hal ini, objek dari kelas UserFactory
terlihat seperti kelas User
dengan metode statis untuk mendapatkan daftar pengguna dan metode khusus untuk membuat entitas baru, dan objeknya berisi semua metode logika bisnis yang diperlukan terkait dengan entitas tertentu.
Tentang ini, saya mengatakan semua yang saya inginkan. Saya berharap dapat berdiskusi dalam komentar.
Perbarui
Saya ingin mengucapkan terima kasih kepada semua orang yang berkomentar. Anda dengan benar mencatat bahwa untuk memecahkan masalah dengan menampilkan nama, ada baiknya menggunakan Pipe
. Saya sepenuhnya setuju dan terkejut sendiri mengapa saya tidak membawa keputusan ini. Namun demikian, tujuan utama artikel ini adalah untuk menunjukkan contoh pembuatan model domain (dalam hal ini, User
), yang dapat dengan mudah merangkum semua logika bisnis yang terkait dengan esensinya. Secara paralel, saya mencoba menyelesaikan masalah yang menyertainya dengan injeksi ketergantungan.