مرحباً بالجميع ، اسمي سيرجي وأنا مطور ويب. سامحني ديمتري كارلوفسكي على المقدمة المقترضة ، لكن منشوراته هي التي ألهمتني لكتابة هذا المقال.
أود اليوم أن أتحدث عن العمل مع البيانات في تطبيقات Angular بشكل عام ونماذج المجال بشكل خاص.
لنفترض أن لدينا قائمة بالمستخدمين التي نتلقاها من الخادم في النموذج
[ { "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 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
إنشاء 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
) ، يجب أن تكون كائنات نموذج المستخدم مسؤولة ليس فقط عن الحصول على بيانات عن نفسها ، ولكن أيضًا عن تغييرها. على سبيل المثال ، قد نتمكن من تغيير صورة ملف تعريف المستخدم. كيف سيكون نموذج المستخدم الموسع بهذه الوظيفة؟
// 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
(بالطبع ، يجب أن يتم تنفيذها أيضًا ، ولكن سيتم ضمان كونها واحدة فقط) أو حتى إنشاء كيان حاقن خارجي ليس عليك تنفيذه ، لكنني أرى الطريقة الأكثر صحة لتفويض إنشاء كائنات فئة 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
بتحويل جميع أعمال تنفيذ التبعيات إلى أكتاف Angular - نحتاج فقط إلى توصيل 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) { } }
في هذه الحالة ، يبدو كائن من فئة UserFactory
مثل فئة User
ذات طرق ثابتة للحصول على قائمة بالمستخدمين وطريقة خاصة لإنشاء كيانات جديدة ، وتحتوي كائناته على جميع طرق منطق الأعمال الضرورية المرتبطة بكيان معين.
على هذا ، قلت كل ما أريد. أتطلع إلى المناقشة في التعليقات.
تحديث
أردت أن أعرب عن امتناني لكل من يعلق. لقد لاحظت حقًا أنه لحل مشكلة عرض الاسم ، سيكون من المفيد استخدام Pipe
. أوافق تمامًا وفوجئت بنفسي لماذا لم أتخذ هذا القرار. ومع ذلك ، فإن الهدف الرئيسي من المقالة هو إظهار مثال على إنشاء نموذج المجال (في هذه الحالة ، User
) ، والذي يمكن أن يشتمل بشكل ملائم على كل منطق الأعمال المرتبط بجوهره. في موازاة ذلك ، حاولت حل المشكلة المصاحبة بحقن التبعية.