NestJS phát triển tập trung vào maintainability và testability trong hệ thống. Để làm như vậy, nó implements bằng các cơ chế khác nhau như Dependency Injection. Trong bài hôm nay, chúng ta cùng tìm hiểu cách mà thằng NestJS này giải quyết dependency đồng thời tìm hiểu cấu trúc Module mà nó xây dựng.
Theo dõi source code tại đây, branch dev nha
Link postman để test cho dễ.
Contents
Reasons to implement Dependency Injection
Xem qua ví dụ dưới đây sử dụng expressjs
import { Router } from 'express';
import Controller from '../interfaces/controller.interface';
import AuthenticationService from './authentication.service';
class AuthenticationController implements Controller {
public path = '/auth';
public router = Router();
public authenticationService = new AuthenticationService();
// (...)
}
Đoạn code này nó vào nhược điểm mà khi làm nhiều các bạn mới để ý được.
Mỗi khi chúng ta tạo 1 instance của AuthenticationController
, chúng ta cũng tạo mới AuthenticationService
. Nếu sử dụng cách trên để áp dụng cho tất cả các controller có sử đụng đến AuthenticationService
, thì mỗi controller sẽ nhận được 1 instance riêng biệt. Và nó có thể trở nên khó bảo trì theo thời gian.
Ngoài ra, khi chạy test cũng bị ảnh hưởng. Mặc dù có thể mock AuthenticationService
trước khi khởi tạo controller ở trên, những đó chỉ là giải pháp nhất thời chớ không được tối ưu và sử dụng nhiều.
Một trong những nguyên tắc SOLID gọi là Inversion of Control (IoC)
Nói rằng các module cấp cao không nên phụ thuộc vào các module cấp thấp. Một cách đơn giản để làm được như vậy là tạo các instance của dependency trước, và gán chúng thông qua contructor
import { Router } from 'express';
import Controller from '../interfaces/controller.interface';
import AuthenticationService from './authentication.service';
class AuthenticationController implements Controller {
public path = '/auth';
public router = Router();
public authenticationService: AuthenticationService;
constructor(authenticationService: AuthenticationService) {
this.authenticationService = authenticationService;
}
// (...)
}
const authenticationService = new AuthenticationService();
const authenticationController = new AuthenticationController(
authenticationService
);
Trong khi thực hiện những điều trên giúp chúng tôi khắc phục các vấn đề đã nêu, thì còn lâu mới tối ưu. Đây là lý do tại sao NestJS triển khai cơ chế Dependency Injection cấp tự động tất cả các phụ thuộc cần thiết.
Dependency Injection in NestJS
Tham khảo một controller tương tự mà mình đã hướng dẫn tại đây
import { Controller } from '@nestjs/common';
import { AuthService } from '../services/auth.service';
@Controller('auth')
export class AuthController {
constructor(private readonly authService: AuthService) {}
// (...)
}
Thay vì phải khởi tạo biến authService như phần trên, thì ta chỉ cần khai báo private readonly
và sử dụng bình thường thôi, khá là thuận tiện. PHP cũng đã chuyển qua kiểu này rồi, không có readonly thôi.
NestJS không những hỗ trợ cho @Controller
và còn hỗ trợ cho những thằng khác như @Injectable
:
...
@Injectable()
export class AuthService {
constructor(
private readonly userService: UserService,
private readonly jwtService: JwtService,
) {}
// (...)
}
Chúng ta cũng không cần quan tâm lắm khi dịch từ TypeScript ra đâu, nhiều khi mình xem mà mình còn không hiểu nó là gì mà, nhiều lúc debug chỉ biết cầu nguyện thôi.
Modules
Module là 1 phần quan trong trong ứng dụng, nơi chứa các chức năng xoay quanh 1 tính năng cụ thể.
Mọi project đều có 1 module gốc. Nó đóng vai trò là điểm khởi đầu cho Nestjs khi khởi tạo đó là AppModule.
app.module.ts
import { Module } from '@nestjs/common';
import { PostModule } from './post/post.module';
import { UserModule } from './user/user.module';
@Module({
imports: [
PostModule,
UserModule,
],
...
})
export class AppModule {}
NestJS sử dụng cái AppModule này làm module gốc để resolve các module khác và những dependencies của nó.
@Global() decorator
Mặc dù việc tạo các module global không được khuyến khích và coi như là tệ, nhưng chúng ta vẫn có thể dùng, và có mình dùng nữa nè, kkk
Khi muốn set các provider có thể khả dụng ở mọi nơi, ở những module khác, bạn sử dụng @Global()
decorator.
@Global()
@Module({
imports: [],
providers: [UserService],
exports: [UserService]
})
export class UserModule {}
Bây giờ chúng ta không cần import UserModule
vẫn có thể sử dụng UserService
Kết luận
Trong bài này không giúp ích được nhiều trong quá trình xây dựng 1 ứng dụng, nhưng nó giúp chúng ta hiểu được phần nào cách hoạt động với các module và resolve các dependency của chúng.
Nguồn tham khảo:
Rất mong được sự ủng hộ của mọi người để mình có động lực ra những bài viết tiếp theo.
{\__/}
( ~.~ )
/ > ♥️ I LOVE YOU 3000
JUST DO IT!