Cách tránh circular dependencies trong Nestjs

4 min read

nestjs

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 UserServiceImageService 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.

Avatar photo

Clean Code: Nguyên tắc viết hàm trong lập trình…

Trong quá trình phát triển phần mềm, việc viết mã nguồn dễ đọc, dễ hiểu là yếu tố then chốt để đảm bảo code...
Avatar photo Dat Tran Thanh
3 min read

Clean Code: Nguyên tắc comment trong lập trình

Trong lập trình, code không chỉ là một tập hợp các câu lệnh để máy tính thực thi, mà còn là một hình thức...
Avatar photo Dat Tran Thanh
3 min read

Clean Code: Nguyên tắc xử lý lỗi (Error Handling)

Trong quá trình phát triển phần mềm, việc xử lý lỗi không chỉ là một phần quan trọng mà còn ảnh hưởng trực tiếp...
Avatar photo Dat Tran Thanh
4 min read

Leave a Reply

Your email address will not be published. Required fields are marked *