💉 OOP trong NestJS (Phần 4): Dependency Injection
🎯 Mục tiêu: Hiểu bản chất DI, IoC Container, và hoàn thiện inject
IUserRepositoryvàoUserService.
📌 Recap từ Phần 1-3
| Phần | Đã học | UserService evolution |
|---|---|---|
| 1 | Class vs Function | Functional → Class-based |
| 2 | Encapsulation, Decorators | Thêm @Injectable(), private methods |
| 3 | Interface, Abstract Class | Tạo IUserRepository, BaseRepository |
Vấn đề cuối Phần 3: Làm sao inject IUserRepository vào UserService?
@Injectable()
export class UserService {
constructor(
private readonly userRepository: IUserRepository // ← Làm sao inject?
) {}
}
1. TẠI SAO CẦN DEPENDENCY INJECTION?
1.1 Vấn đề với Manual Instantiation
// ❌ Không có DI - Hard dependencies
class UserService {
private userRepository: InMemoryUserRepository;
private logger: ConsoleLogger;
private emailService: SendGridEmailService;
constructor() {
// Tự tạo dependencies
this.userRepository = new InMemoryUserRepository();
this.logger = new ConsoleLogger();
this.emailService = new SendGridEmailService('api-key-hardcoded');
}
async create(dto: CreateUserDto) {
this.logger.log('Creating user...');
const user = await this.userRepository.create(dto);
await this.emailService.send(user.email, 'Welcome!');
return user;
}
}
// Khi test:
const service = new UserService();
// ❌ Không thể mock userRepository
// ❌ Không thể mock emailService (thực sự gửi email!)
// ❌ Muốn đổi sang PostgreSQL? Phải sửa code!
1.2 Với Dependency Injection
// ✅ Có DI - Dependencies được inject
class UserService {
constructor(
private userRepository: IUserRepository,
private logger: ILogger,
private emailService: IEmailService
) {}
async create(dto: CreateUserDto) {
this.logger.log('Creating user...');
const user = await this.userRepository.create(dto);
await this.emailService.send(user.email, 'Welcome!');
return user;
}
}
// Production:
const service = new UserService(
new PostgresUserRepository(db),
new CloudWatchLogger(),
new SendGridEmailService(process.env.SENDGRID_KEY)
);
// Testing:
const service = new UserService(
new MockUserRepository(),
new MockLogger(),
new MockEmailService() // Không gửi email thật!
);
1.3 Lợi ích của DI
| Lợi ích | Giải thích |
|---|---|
| Loose Coupling | UserService không biết về implementation cụ thể |
| Testability | Dễ dàng inject mocks |
| Flexibility | Đổi implementation không sửa code |
| Single Responsibility | Tạo dependencies không phải việc của UserService |
2. IOC CONTAINER - NGƯỜI QUẢN LÝ
2.1 IoC là gì?
Inversion of Control: Đảo ngược quyền kiểm soát việc tạo dependencies.
Không IoC:
UserService tự tạo dependencies
Có IoC:
Container tạo dependencies → inject vào UserService
2.2 NestJS IoC Container hoạt động thế nào?
// 1. Khai báo providers trong Module
@Module({
providers: [
UserService, // Class được quản lý
UserRepository, // Dependency
Logger, // Dependency
],
controllers: [UserController]
})
export class UserModule {}
// 2. NestJS đọc metadata từ @Injectable()
@Injectable()
export class UserService {
constructor(
private userRepository: UserRepository,
private logger: Logger
) {}
}
// 3. Khi cần UserService, NestJS:
// a. Tìm UserRepository trong providers
// b. Tìm Logger trong providers
// c. Tạo instance của cả hai
// d. Inject vào UserService constructor
// e. Trả về UserService instance
2.3 Token - Chìa Khóa Của DI
// Token = ID để NestJS tìm provider
// Token kiểu 1: Class reference (default)
@Injectable()
class UserRepository {}
providers: [UserRepository] // Token là UserRepository class
// Token kiểu 2: String
providers: [
{
provide: 'USER_REPOSITORY', // String token
useClass: UserRepository
}
]
// Token kiểu 3: Symbol (unique)
const USER_REPO = Symbol('USER_REPOSITORY');
providers: [
{
provide: USER_REPO,
useClass: UserRepository
}
]
3. CÁC LOẠI PROVIDERS
3.1 Standard Provider (useClass)
// Cách ngắn
providers: [UserService]
// Tương đương với
providers: [
{
provide: UserService, // Token
useClass: UserService // Implementation
}
]
3.2 Value Provider (useValue)
// Inject giá trị cố định
providers: [
{
provide: 'CONFIG',
useValue: {
database: 'postgres',
port: 5432,
debug: process.env.NODE_ENV !== 'production'
}
}
]
// Sử dụng
@Injectable()
class DatabaseService {
constructor(@Inject('CONFIG') private config: DatabaseConfig) {
console.log(this.config.database); // 'postgres'
}
}
3.3 Factory Provider (useFactory)
// Tạo provider với logic phức tạp
providers: [
{
provide: 'DATABASE_CONNECTION',
useFactory: async (config: ConfigService) => {
const connection = await createConnection({
host: config.get('DB_HOST'),
port: config.get('DB_PORT'),
username: config.get('DB_USER'),
password: config.get('DB_PASS'),
database: config.get('DB_NAME'),
});
return connection;
},
inject: [ConfigService] // Dependencies của factory
}
]
3.4 Alias Provider (useExisting)
// Alias cho provider khác
providers: [
UserRepository,
{
provide: 'AliasRepo',
useExisting: UserRepository // Trỏ đến cùng instance
}
]
3.5 Bảng So Sánh
| Provider Type | Dùng khi | Ví dụ |
|---|---|---|
useClass | Inject class implementation | Service, Repository |
useValue | Inject giá trị tĩnh | Config, constants |
useFactory | Cần logic khởi tạo | DB connection, async init |
useExisting | Alias cho provider có sẵn | Backward compatibility |
4. INJECT INTERFACE - VẤN ĐỀ VÀ GIẢI PHÁP
4.1 Vấn đề: Interface không tồn tại ở Runtime
// ❌ Không hoạt động - Interface biến mất sau compile
@Injectable()
export class UserService {
constructor(private userRepository: IUserRepository) {}
}
// Error: Nest can't resolve dependencies of UserService
// IUserRepository không còn ở runtime!
4.2 Giải pháp 1: String Token
// interfaces/user-repository.interface.ts
export const USER_REPOSITORY = 'USER_REPOSITORY';
export interface IUserRepository {
create(user: CreateUserData): Promise<User>;
findByEmail(email: string): Promise<User | null>;
// ...
}
// user.module.ts
@Module({
providers: [
UserService,
{
provide: USER_REPOSITORY,
useClass: InMemoryUserRepository
}
]
})
export class UserModule {}
// user.service.ts
@Injectable()
export class UserService {
constructor(
@Inject(USER_REPOSITORY)
private readonly userRepository: IUserRepository
) {}
}
4.3 Giải pháp 2: Abstract Class as Token
// Abstract class TỒN TẠI ở runtime → có thể dùng làm token
export abstract class UserRepository {
abstract create(user: CreateUserData): Promise<User>;
abstract findByEmail(email: string): Promise<User | null>;
abstract findById(id: string): Promise<User | null>;
abstract update(id: string, data: Partial<User>): Promise<User>;
abstract delete(id: string): Promise<void>;
}
// Implementation
@Injectable()
export class InMemoryUserRepository extends UserRepository {
private users = new Map<string, User>();
async create(user: CreateUserData): Promise<User> {
// implementation
}
// ... other methods
}
// Module
@Module({
providers: [
UserService,
{
provide: UserRepository, // Abstract class as token
useClass: InMemoryUserRepository
}
]
})
export class UserModule {}
// UserService - Không cần @Inject!
@Injectable()
export class UserService {
constructor(private readonly userRepository: UserRepository) {}
}
4.4 So sánh 2 Giải pháp
| Approach | Ưu điểm | Nhược điểm |
|---|---|---|
| String Token | Rõ ràng, flexible | Cần @Inject decorator |
| Abstract Class | Không cần @Inject | Ít flexible hơn |
5. HOÀN THIỆN USERSERVICE
5.1 Folder Structure
src/
├── users/
│ ├── interfaces/
│ │ └── user-repository.interface.ts
│ ├── repositories/
│ │ ├── base.repository.ts
│ │ ├── in-memory-user.repository.ts