Đã 14 năm kể từ ngày mà Node.js ra đời, ở hiện tại thì nhà nhà dùng Node.js, người người dùng Node.js. Số lượng dev Node giờ đây đã đông như quân Nguyên, cũng vì vâỵ mà số lượng các công ty doanh nghiệp sử dụng Nodejs cũng tăng lên nhanh chóng, và vì thế mà Nodejs phải hỗ trợ được nhiều khía cạnh hơn so với trước đây.
Vấn đề đầu tiên mà hầu như dev Nodejs nào cũng nhận thấy đó là architecture. Khác với Laravel của PHP hay Django của Python, các framework nổi bật của Nodejs như Express hay Fastiify đều không hỗ trợ một cấu trúc code tiêu chuẩn mà để cho các dev tùy ý custom. Điều này dẫn đến việc một dự án mà người đến và đi liên tục sẽ khiến cho người sau code theo chuẩn khác với người trước, và code của dự án đó sẽ dần dần rối và rác. Thế nên NestJS đã ra đời để giải quyết bài toán về architecture.
Nest provides an out-of-the-box application architecture which allows developers and teams to create highly testable, scalable, loosely coupled, and easily maintainable applications. The architecture is heavily inspired by Angular.
Bên cạnh đó Nest cũng cung cấp một design pattern tiêu chuẩn là Dependency Injection. Với những ai đã từng làm C# hay Java thì chắc ít nhiều cũng đã từng nghe qua hay sử dụng pattern này. Chữ D trong Dependency cũng là chữ D trong bộ nguyên tắc SOLID.
Nguyên tắc D của SOLID được phát biểu như sau:
Các module cấp cao không nên phụ thuộc vào các module cấp thấp, mà chỉ nên phụ thuộc vào abstraction.
Để làm rõ nguyên tắc này và ý tưởng của pattern DI mình sẽ đi vào một ví dụ như sau:
Một ứng dụng Backend thông thường sẽ có kiến trúc theo các lớp như sau:
- repositories: thực hiện việc tương tác trực tiếp với DB.
- services: đơn vị thực thi logic, nằm giữa controller và repositories.
- controllers: nhận request http từ một route cụ thể và chỉ định service xử lý yêu cầu và chuẩn bị gửi phản hồi về.
Ví dụ như ban đầu ta triển khai một model là User với cơ sở dữ liệu là MySQL. Như vậy, ta sẽ có lớp User để định nghĩa các trường hay thuộc tính cần thiết. Một interface cho UserRepository và một triển khai để hỗ trợ giao tiếp với MySQL - UserMySQLRepository. Lớp UserService nằm giữa controller và repositories nên nó sẽ phụ thuộc vào UserRepository:
// ./user/user.entity.ts
class User {
/* skipped for brevity */
}
// ./user/user-repository.interface.ts
interface UserRepository {
findAll(): User[];
}
// ./user/user-repository-mysql.ts
class UserMySQLRepository implements UserRepository {
findAll(): User[] {
/* skipped for brevity */
}
}
// ./user/user.service.ts
class UserService {
userRepository: UserRepository;
constructor() {
this.userRepository = new UserMySQLRepository();
}
getUsers(): User[] {
return this.userRepository.findAll();
}
}
Khi dự án phát triển và rồi ta cần chuyển sang sử dụng MongoDB thay cho MySQL. Như vậy lúc này ta cần tạo một triển khai cho MongoDB:
// ./user/user-repository-mongodb.ts
class UserMongoDBRepository implements UserRepository {
findAll(): User[] {
/* skipped for brevity */
}
}
Điều này đồng nghĩa với việc chúng ta phải thay đổi phụ thuộc UserMySQLRepository() trong lớp UserService thành UserMongoDBRepository:
Nhưng, giả sử trong trường hợp UserRepository đã được sử dụng trong nhiều service khác, ví dụ như ProfileService, thì lúc đó mối quan hệ trong dự án sẽ như thế này:
Như thế chúng ta sẽ phải đi sửa lại trên từng service, công việc vừa tốn thời gian vừa dễ gây ra lỗi.
Đồng thời trong trường hợp ta cần viết unit test cho UserService, chúng ta phải giả lập lại từ UserRepository, ví dụ như hàm test của chúng ta có dạng thế này:
const EXPECTED_USERS = [{/* user 1 */ }, {/* user 2*/ }];
jest.mock('./user/user-repository-mysql.ts', () => {
return {
findAll: () => {
return EXPECTED_USERS;
},
};
});
describe('UserService', () => {
it('get all users', () => {
const useService = new UserService();
expect(useService.getUsers()).toEqual(EXPECTED_USERS);
});
});
Như vậy một lần nữa khi ta chuyển từ MySQL sang MongoDB ta cũng phải thay đổi lại ở các unit test.
Để khắc phục nhược điểm trên pattern DI đưa cho chúng ta ý tưởng rằng thay vì sử dụng UserReposity như một lớp phụ thuộc chúng ta có thể truyền nó vào constructor của UserService.
export class UserService {
constructor(private userRepository: UserRepository) {}
getUsers(): User[] {
return this.userRepository.findAll();
}
}
Với refactor này chúng ta có thể viết lại unit test như sau:
const EXPECTED_USERS = [{/* user 1 */ }, {/* user 2*/ }];
describe('UserService', () => {
it('get all users', () => {
const useService = new UserService({
findAll(): User[] {
return EXPECTED_USERS;
},
});
expect(useService.getUsers()).toEqual(EXPECTED_USERS);
});
});
Nếu bạn chưa hình dung rõ sự thay đổi trong việc triển khai DI pattern thì hãy nhìn vào bức hình bên dưới:
Với việc triển khai DI sẽ giúp cho đoạn code dễ hiểu và bảo trì hơn, tuy nhiên chắc hẵn sẽ có nhiều bạn thắc mắc rằng với đoạn code trên thì làm sao xác định được đang kết nối đến cơ sở dữ liệu nào. Đây chính là lúc mà Nestjs phát huy sức mạnh của nó:
@Module({
imports: [],
providers: [
{
provide: USER_REPOSITORY_TOKEN,
useValue: UserMySQLRepository,
},
UserService,
],
})
export class AppModule {}
AppModule sẽ cung cấp hai provider và trong trường hợp này thì UserSerrvice đang phụ thuộc vào triển khai của UserRepository, như vậy chúng ta sẽ inject phụ thuộc đó vào UserService như sau:
export const USER_REPOSITORY_TOKEN = Symbol('USER_REPOSITORY_TOKEN');
@Injectable()
export class UserService {
constructor(
@Inject(USER_REPOSITORY_TOKEN) private userRepository: UserRepository,
) {}
getUsers(): User[] {
return this.userRepository.findAll();
}
}
Bây giờ trong trường hợp dự án chúng ta chuyển đổi sang MongoDB thay vì MySQL ta chỉ việc sửa chúng tại một file module đơn giản thế này thôi:
@Module({
imports: [],
providers: [
{
provide: USER_REPOSITORY_TOKEN,
useValue: UserMongoDBRepository,
},
UserService,
],
})
export class AppModule {}
Tham khảo
https://medium.com/geekculture/nestjs-and-dependency-injection-3ce0886148c4
Đọc thêm