Phụ thuộc vòng (circular dependency) xảy ra khi hai class phụ thuộc lẫn nhau. Ví dụ, class A cần class B, và class B cũng cần class A. Phụ thuộc vòng có thể xuất hiện trong Nest giữa các module với nhau và giữa các provider với nhau.
Mặc dù nên tránh phụ thuộc vòng nếu có thể, nhưng không phải lúc nào cũng tránh được. Trong những trường hợp đó, Nest cho phép bạn giải quyết phụ thuộc vòng giữa các provider theo hai cách. Trong chương này, chúng ta sẽ mô tả cách dùng forward referencing (tham chiếu chuyển tiếp) như một kỹ thuật, và dùng class ModuleRef để lấy instance của provider từ DI container như một kỹ thuật khác.
Chúng ta cũng sẽ mô tả cách giải quyết phụ thuộc vòng giữa các module.
Cảnh báo Phụ thuộc vòng cũng có thể phát sinh khi dùng “barrel file”/file index.ts để gom các import lại. Không nên dùng barrel file cho các class module/provider. Ví dụ, không nên dùng barrel file khi import các file trong cùng thư mục với barrel file đó, tức là cats/cats.controller không nên import cats để lấy file cats/cats.service. Xem thêm chi tiết tại GitHub issue này.
Ghi chú người dịch “Barrel file” là file index.ts chỉ có nhiệm vụ re-export mọi thứ trong thư mục (export * from './cats.service'…). Nó tiện khi import ở nơi khác, nhưng lại dễ tạo vòng lặp import ẩn ngay bên trong chính thư mục đó — TypeScript/bundler phải resolve toàn bộ file trong barrel trước, nên hai file cùng thư mục import qua barrel dễ bị “chờ nhau” dẫn đến circular dependency giả (không phải do logic nghiệp vụ, mà do cách tổ chức import).
Forward reference (Tham chiếu chuyển tiếp)
Forward reference cho phép Nest tham chiếu tới các class chưa được định nghĩa bằng hàm tiện ích forwardRef(). Ví dụ, nếu CatsService và CommonService phụ thuộc lẫn nhau, cả hai phía của mối quan hệ đều có thể dùng @Inject() cùng hàm tiện ích forwardRef() để giải quyết phụ thuộc vòng. Nếu không làm vậy, Nest sẽ không thể khởi tạo chúng vì thiếu metadata cần thiết. Ví dụ:
@Injectable()
export class CatsService {
constructor(
@Inject(forwardRef(() => CommonService))
private commonService: CommonService,
) {}
}
@Injectable()
@Dependencies(forwardRef(() => CommonService))
export class CatsService {
constructor(commonService) {
this.commonService = commonService;
}
}
Gợi ý Hàm forwardRef() được import từ package @nestjs/common.
Đó là một phía của mối quan hệ. Giờ hãy làm tương tự với CommonService:
@Injectable()
export class CommonService {
constructor(
@Inject(forwardRef(() => CatsService))
private catsService: CatsService,
) {}
}
@Injectable()
@Dependencies(forwardRef(() => CatsService))
export class CommonService {
constructor(catsService) {
this.catsService = catsService;
}
}
Cảnh báo Thứ tự khởi tạo là không xác định (indeterminate). Hãy chắc chắn code của bạn không phụ thuộc vào việc constructor nào được gọi trước. Việc để phụ thuộc vòng phụ thuộc vào các provider có Scope.REQUEST có thể dẫn đến dependency bị undefined. Xem thêm thông tin tại đây
Ghi chú người dịch Tương tự việc hai package Go import lẫn nhau — Go compiler sẽ báo lỗi thẳng (import cycle not allowed), còn ở Nest thì không lỗi biên dịch nhưng runtime sẽ không biết khởi tạo cái nào trước để lấy metadata của cái kia. forwardRef() giống như bạn khai báo “cứ tin tôi, class này sẽ tồn tại, đừng resolve ngay bây giờ” — trì hoãn việc resolve reference sang lúc DI container đã dựng xong toàn bộ graph.
ModuleRef class alternative (Cách thay thế bằng class ModuleRef)
Một cách khác thay cho forwardRef() là tái cấu trúc code và dùng class ModuleRef để lấy một provider ở một phía của mối quan hệ vòng (vốn dĩ nếu không sẽ là circular). Tìm hiểu thêm về class tiện ích ModuleRef tại đây.
Module forward reference (Tham chiếu chuyển tiếp giữa module)
Để giải quyết phụ thuộc vòng giữa các module, hãy dùng cùng hàm tiện ích forwardRef() ở cả hai phía của mối liên kết giữa các module. Ví dụ:
@Module({
imports: [forwardRef(() => CatsModule)],
})
export class CommonModule {}
Đó là một phía của mối quan hệ. Giờ hãy làm tương tự với CatsModule:
@Module({
imports: [forwardRef(() => CommonModule)],
})
export class CatsModule {}