Trong NestJS, chúng ta có thể phân tách các quan tâm trong ứng dụng thành các class logic và class truy cập. Tuy nhiên, việc xử lý dependencies cần được thực hiện cẩn thận để tránh dependencies vòng lặp, một vấn đề phổ biến có thể gặp trong NestJS. Tránh các dependencies vòng lặp không chỉ giúp code dễ hiểu và dễ chỉnh sửa hơn mà còn cung cấp một kiến trúc tốt hơn cho code backend của chúng ta.
Dependency vòng lặp là gì?
Trong lập trình, dependency vòng lặp xảy ra khi hai hoặc nhiều module phụ thuộc vào nhau trực tiếp hoặc gián tiếp. Ví dụ như A phụ thuộc vào B và B phụ thuộc lại vào A (phụ thuộc trực tiếp), hoặc A phụ thuộc vào B, B phụ thuộc vào C, và C lại phụ thuộc vào A (phụ thuộc gián tiếp).
Trong NestJS, các dependency vòng lặp có thể xảy ra do cách hệ thống dependency injection của NestJS hoạt động. Việc hiểu cách NestJS xử lý phụ thuộc giúp ta nhận biết tại sao và làm thế nào mà các tham chiếu vòng có thể xảy ra. Tránh các phụ thuộc vòng lặp là quan trọng để giữ source code dễ hiểu và dễ bảo trì.
Hệ thống dependency injection của Nestjs
Ví dụ ta có UserService
import { Injectable } from '@nestjs/common';
@Injectable()
export class UserService {
constructor() {}
public async getUser(userId: string) {
...
}
...
}
Decorator @Injectable() được sử dụng trong định nghĩa của class UserService đánh dấu class đó là một provider mà hệ thống dependency injection của NestJS có thể inject vào, tức là nó nên được quản lý bởi container.
Trong NestJS, mỗi module có injector riêng có thể truy cập vào container. Khi khai báo một module, bạn phải chỉ định các provider sẽ có sẵn cho module đó, ngoại trừ trường hợp provider đó là global provider. Ví dụ UserModule
import { Module } from '@nestjs/common';
import { UserService } from './user.service';
import { UserController } from './user.controller';
@Module({
providers: [UserService],
controllers: [UserController],
exports: [UserService],
})
export class UserModule {}
Dependency vòng lặp phát sinh như thế nào
Ví dụ ImageService
import { Injectable } from '@nestjs/common';
import { UserService } from '../user/user.service';
import { File } from './interfaces/image.interface';
@Injectable()
export class ImageService {
constructor(private readonly userService: UserService) {}
public getById(pictureId: string): File {
// not real implementation
return {
id: "https://example/" + pictureId,
};
}
public async getUserProfilePicture(userId: string): Promise<File> {
const user = await this.userService.getUserById(userId);
return this.getById(user.profilePictureId);
}
}
ImageService
cần injected dependency UserService
injected dependency để có thể sử dụng
import { Injectable } from '@nestjs/common';
import { ImageService } from '../image-service/image.service';
@Injectable()
export class UserService {
constructor(private readonly imageService: ImageService) {}
public async getUserById(userId: string) {
return {
id: userId,
name: 'Sam',
profilePictureId: 'kdkf43',
};
}
}
Chúng ta có dependency vòng lặp trong trường hợp này, vì UserService
và ImageService
phụ thuộc lẫn nhau ( UserService
→ ImageService
→ UserService
).
Tránh dependency vòng lặp bằng cách tái cấu trúc code
Document của NestJS khuyên rằng nên tránh dependency vòng lặp nếu có thể.
Chúng ta có thể loại bỏ dependency vòng lặp dễ vàng trong ví dụ này. Mối quan hệ giữa 2 class có thể biểu diễn như sau: UserService → ImageService và ImageService → UserService
Để phá vỡ chu trình trên chúng ta nên tách cách tính năng chung từ 2 class ImageService và UserService rồi tạo ra 1 class thứ 3 ProfileImageService
phụ thuộc vào cả hai UserService
và ImageService
.
ProfilePictureService
sẽ có module riêng được định nghĩa như sau:
import { Module } from '@nestjs/common';
import { FileModule } from '../image-service/image.module';
import { UserModule } from '../user/user.module';
import { ProfileImageService } from './profile-image.service';
@Module({
imports: [ImageModule, UserModule],
providers: [ProfilePictureService],
})
export class ProfileImageModule {}
Lưu ý rằng module này import cả ImageModule
và UserModule
. Cả hai module đã import đều phải export các service mà chúng tôi muốn sử dụng ở ProfileImageService
Cuối cùng ProfileImageService
sẽ như sau
import { Injectable } from '@nestjs/common';
import { File } from '../image-service/interfaces/image.interface';
import { ImageService } from '../image-service/image.service';
import { UserService } from '../user/user.service';
@Injectable()
export class ProfileImageService {
constructor(
private readonly imageService: ImageService,
private readonly userService: UserService,
) {}
public async addUserProfileImage(userId: string, pictureId: string) {
const image = await this.imageService.getById(pictureId);
// update user with the picture url
return { id: userId, name: 'Sam', profilePictureId: image.id };
}
public async getUserProfilePicture(userId: string): Promise<File> {
const user = await this.userService.getUserById(userId);
return this.fileService.getById(user.profilePictureId);
}
}
Làm việc với dependecy vòng lặp với forward references
Lý tưởng nhất là nên tránh dependecy vòng lặp, nhưng trong những trường hợp không thể thực hiện được, Nest sẽ cung cấp cách để giải quyết chúng.
Forward references cho phép Nest tham chiếu các lớp chưa được xác định bằng cách sử dụng hàm tiện ích forwardRef()
Ví dụ: chúng ta có thể sửa đổi UserService
và ImageService
như sau:
import { forwardRef, Inject, Injectable } from '@nestjs/common';
import { ImageService } from '../image-service/image.service';
@Injectable()
export class UserService {
constructor(
@Inject(forwardRef(() => ImageService))
private readonly imageService: ImageService) {}
public async getUserById(userId: string) {
return {
id: userId,
name: 'Sam',
profilePictureId: 'kdkf43',
};
}
}
import { forwardRef, Inject, Injectable } from '@nestjs/common';
import { UserService } from '../user/user.service';
import { File } from './interfaces/image.interface';
@Injectable()
export class ImageService {
constructor(
@Inject(forwardRef(() => UserService))
private readonly userService: UserService) {}
public getById(pictureId: string): File {
// not real implementation
return {
id: "https://example/" + pictureId,
};
}
public async getUserProfilePicture(userId: string): Promise<File> {
const user = await this.userService.getUserById(userId);
return this.getById(user.profilePictureId);
}
}
Forward references cho modules
forwardRef()
cũng có thể được sử dụng để giải quyết dependency vòng lặp giữa các module, nhưng nó phải được sử dụng ở cả hai phía của liên kết module. Ví dụ:
@Module({
imports: [forwardRef(() => SecondCircularModule)],
})
export class FirstCircularModule {}
Kết luận
Trong bài viết này, chúng ta đã tìm hiểu về phần dependency vòng lặp là gì, cách hoạt động của tính năng dependency injection trong NestJS và các vấn đề về dependency vòng lặp có thể phát sinh như thế nào.
Chúng tôi cũng đã tìm hiểu cách có thể tránh dependency vòng lặp trong NestJS và lý do tại sao chúng tôi nên luôn cố gắng tránh điều đó. Hy vọng rằng bây giờ bạn đã biết cách giải quyết vấn đề này trong trường hợp không thể tránh khỏi bằng việc sử dụng forward references.