Added AI skills
This commit is contained in:
@@ -0,0 +1,349 @@
|
||||
---
|
||||
id: solid-dip-injection
|
||||
title: SOLID - Dependency Inversion (Injection)
|
||||
category: solid-principles
|
||||
priority: critical
|
||||
tags: [SOLID, DIP, dependency-injection, testability]
|
||||
related: [solid-dip-abstractions, solid-srp-class, core-composition]
|
||||
---
|
||||
|
||||
# Dependency Inversion Principle - Dependency Injection
|
||||
|
||||
Dependencies should be injected from outside rather than created inside a class. This enables loose coupling, testability, and flexibility in how dependencies are provided.
|
||||
|
||||
## Bad Example
|
||||
|
||||
```typescript
|
||||
// Anti-pattern: Creating dependencies inside the class
|
||||
|
||||
class UserRegistrationService {
|
||||
private userRepository: UserRepository;
|
||||
private passwordHasher: PasswordHasher;
|
||||
private emailService: EmailService;
|
||||
private logger: Logger;
|
||||
private analyticsService: AnalyticsService;
|
||||
|
||||
constructor() {
|
||||
// Dependencies created inside - tight coupling
|
||||
this.userRepository = new PostgresUserRepository(
|
||||
new PostgresConnection('localhost', 5432, 'mydb')
|
||||
);
|
||||
this.passwordHasher = new BcryptPasswordHasher(10);
|
||||
this.emailService = new SendGridEmailService(process.env.SENDGRID_KEY!);
|
||||
this.logger = new WinstonLogger('UserRegistration');
|
||||
this.analyticsService = new MixpanelAnalytics(process.env.MIXPANEL_TOKEN!);
|
||||
}
|
||||
|
||||
async register(email: string, password: string): Promise<User> {
|
||||
this.logger.info(`Registering user: ${email}`);
|
||||
|
||||
const existingUser = await this.userRepository.findByEmail(email);
|
||||
if (existingUser) {
|
||||
throw new Error('User already exists');
|
||||
}
|
||||
|
||||
const hashedPassword = await this.passwordHasher.hash(password);
|
||||
const user = await this.userRepository.create({ email, password: hashedPassword });
|
||||
|
||||
await this.emailService.send({
|
||||
to: email,
|
||||
subject: 'Welcome!',
|
||||
body: 'Thanks for registering'
|
||||
});
|
||||
|
||||
await this.analyticsService.track('user_registered', { userId: user.id });
|
||||
|
||||
return user;
|
||||
}
|
||||
}
|
||||
|
||||
// Problems:
|
||||
// 1. Cannot test without real Postgres, SendGrid, Mixpanel
|
||||
// 2. Cannot reuse with different implementations
|
||||
// 3. Hard-coded configuration scattered through constructors
|
||||
// 4. Class responsible for creating its dependencies
|
||||
```
|
||||
|
||||
## Good Example
|
||||
|
||||
```typescript
|
||||
// Correct approach: Dependencies injected from outside
|
||||
|
||||
// Interfaces for all dependencies
|
||||
interface UserRepository {
|
||||
findByEmail(email: string): Promise<User | null>;
|
||||
findById(id: string): Promise<User | null>;
|
||||
create(data: CreateUserData): Promise<User>;
|
||||
update(id: string, data: Partial<User>): Promise<User>;
|
||||
}
|
||||
|
||||
interface PasswordHasher {
|
||||
hash(password: string): Promise<string>;
|
||||
verify(password: string, hash: string): Promise<boolean>;
|
||||
}
|
||||
|
||||
interface EmailService {
|
||||
send(options: EmailOptions): Promise<void>;
|
||||
}
|
||||
|
||||
interface Logger {
|
||||
info(message: string, meta?: Record<string, any>): void;
|
||||
error(message: string, error?: Error, meta?: Record<string, any>): void;
|
||||
warn(message: string, meta?: Record<string, any>): void;
|
||||
}
|
||||
|
||||
interface AnalyticsService {
|
||||
track(event: string, properties?: Record<string, any>): Promise<void>;
|
||||
identify(userId: string, traits?: Record<string, any>): Promise<void>;
|
||||
}
|
||||
|
||||
// Service with constructor injection
|
||||
class UserRegistrationService {
|
||||
constructor(
|
||||
private readonly userRepository: UserRepository,
|
||||
private readonly passwordHasher: PasswordHasher,
|
||||
private readonly emailService: EmailService,
|
||||
private readonly logger: Logger,
|
||||
private readonly analyticsService: AnalyticsService
|
||||
) {}
|
||||
|
||||
async register(email: string, password: string): Promise<User> {
|
||||
this.logger.info('Registering user', { email });
|
||||
|
||||
const existingUser = await this.userRepository.findByEmail(email);
|
||||
if (existingUser) {
|
||||
throw new UserAlreadyExistsError(email);
|
||||
}
|
||||
|
||||
const hashedPassword = await this.passwordHasher.hash(password);
|
||||
const user = await this.userRepository.create({
|
||||
email,
|
||||
password: hashedPassword,
|
||||
createdAt: new Date()
|
||||
});
|
||||
|
||||
await this.sendWelcomeEmail(user);
|
||||
await this.trackRegistration(user);
|
||||
|
||||
this.logger.info('User registered successfully', { userId: user.id });
|
||||
return user;
|
||||
}
|
||||
|
||||
private async sendWelcomeEmail(user: User): Promise<void> {
|
||||
try {
|
||||
await this.emailService.send({
|
||||
to: user.email,
|
||||
subject: 'Welcome!',
|
||||
body: 'Thanks for registering'
|
||||
});
|
||||
} catch (error) {
|
||||
this.logger.error('Failed to send welcome email', error as Error, { userId: user.id });
|
||||
// Don't fail registration if email fails
|
||||
}
|
||||
}
|
||||
|
||||
private async trackRegistration(user: User): Promise<void> {
|
||||
try {
|
||||
await this.analyticsService.identify(user.id, { email: user.email });
|
||||
await this.analyticsService.track('user_registered', { userId: user.id });
|
||||
} catch (error) {
|
||||
this.logger.error('Failed to track registration', error as Error, { userId: user.id });
|
||||
// Don't fail registration if analytics fails
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Concrete implementations
|
||||
class PostgresUserRepository implements UserRepository {
|
||||
constructor(private db: DatabaseConnection) {}
|
||||
|
||||
async findByEmail(email: string): Promise<User | null> {
|
||||
const result = await this.db.query('SELECT * FROM users WHERE email = $1', [email]);
|
||||
return result.rows[0] || null;
|
||||
}
|
||||
|
||||
async findById(id: string): Promise<User | null> {
|
||||
const result = await this.db.query('SELECT * FROM users WHERE id = $1', [id]);
|
||||
return result.rows[0] || null;
|
||||
}
|
||||
|
||||
async create(data: CreateUserData): Promise<User> {
|
||||
const result = await this.db.query(
|
||||
'INSERT INTO users (email, password, created_at) VALUES ($1, $2, $3) RETURNING *',
|
||||
[data.email, data.password, data.createdAt]
|
||||
);
|
||||
return result.rows[0];
|
||||
}
|
||||
|
||||
async update(id: string, data: Partial<User>): Promise<User> {
|
||||
// Implementation
|
||||
}
|
||||
}
|
||||
|
||||
class BcryptPasswordHasher implements PasswordHasher {
|
||||
constructor(private rounds: number = 10) {}
|
||||
|
||||
async hash(password: string): Promise<string> {
|
||||
return bcrypt.hash(password, this.rounds);
|
||||
}
|
||||
|
||||
async verify(password: string, hash: string): Promise<boolean> {
|
||||
return bcrypt.compare(password, hash);
|
||||
}
|
||||
}
|
||||
|
||||
// Dependency Injection Container (manual)
|
||||
class Container {
|
||||
private services: Map<string, any> = new Map();
|
||||
|
||||
register<T>(key: string, factory: () => T): void {
|
||||
this.services.set(key, factory);
|
||||
}
|
||||
|
||||
resolve<T>(key: string): T {
|
||||
const factory = this.services.get(key);
|
||||
if (!factory) {
|
||||
throw new Error(`Service not registered: ${key}`);
|
||||
}
|
||||
return factory();
|
||||
}
|
||||
}
|
||||
|
||||
// Composition root - wire up all dependencies
|
||||
function configureContainer(): Container {
|
||||
const container = new Container();
|
||||
|
||||
// Register infrastructure
|
||||
container.register('DatabaseConnection', () =>
|
||||
new PostgresConnection(process.env.DATABASE_URL!)
|
||||
);
|
||||
|
||||
container.register('Logger', () =>
|
||||
new WinstonLogger({ level: process.env.LOG_LEVEL || 'info' })
|
||||
);
|
||||
|
||||
// Register repositories
|
||||
container.register('UserRepository', () =>
|
||||
new PostgresUserRepository(container.resolve('DatabaseConnection'))
|
||||
);
|
||||
|
||||
// Register services
|
||||
container.register('PasswordHasher', () =>
|
||||
new BcryptPasswordHasher(12)
|
||||
);
|
||||
|
||||
container.register('EmailService', () =>
|
||||
new SendGridEmailService(process.env.SENDGRID_API_KEY!)
|
||||
);
|
||||
|
||||
container.register('AnalyticsService', () =>
|
||||
new MixpanelAnalytics(process.env.MIXPANEL_TOKEN!)
|
||||
);
|
||||
|
||||
// Register application services
|
||||
container.register('UserRegistrationService', () =>
|
||||
new UserRegistrationService(
|
||||
container.resolve('UserRepository'),
|
||||
container.resolve('PasswordHasher'),
|
||||
container.resolve('EmailService'),
|
||||
container.resolve('Logger'),
|
||||
container.resolve('AnalyticsService')
|
||||
)
|
||||
);
|
||||
|
||||
return container;
|
||||
}
|
||||
|
||||
// Application startup
|
||||
const container = configureContainer();
|
||||
const registrationService = container.resolve<UserRegistrationService>('UserRegistrationService');
|
||||
|
||||
// Testing is now trivial with mock implementations
|
||||
describe('UserRegistrationService', () => {
|
||||
let service: UserRegistrationService;
|
||||
let mockUserRepository: jest.Mocked<UserRepository>;
|
||||
let mockPasswordHasher: jest.Mocked<PasswordHasher>;
|
||||
let mockEmailService: jest.Mocked<EmailService>;
|
||||
let mockLogger: jest.Mocked<Logger>;
|
||||
let mockAnalytics: jest.Mocked<AnalyticsService>;
|
||||
|
||||
beforeEach(() => {
|
||||
mockUserRepository = {
|
||||
findByEmail: jest.fn(),
|
||||
findById: jest.fn(),
|
||||
create: jest.fn(),
|
||||
update: jest.fn()
|
||||
};
|
||||
mockPasswordHasher = {
|
||||
hash: jest.fn(),
|
||||
verify: jest.fn()
|
||||
};
|
||||
mockEmailService = { send: jest.fn() };
|
||||
mockLogger = { info: jest.fn(), error: jest.fn(), warn: jest.fn() };
|
||||
mockAnalytics = { track: jest.fn(), identify: jest.fn() };
|
||||
|
||||
service = new UserRegistrationService(
|
||||
mockUserRepository,
|
||||
mockPasswordHasher,
|
||||
mockEmailService,
|
||||
mockLogger,
|
||||
mockAnalytics
|
||||
);
|
||||
});
|
||||
|
||||
it('should register a new user', async () => {
|
||||
mockUserRepository.findByEmail.mockResolvedValue(null);
|
||||
mockPasswordHasher.hash.mockResolvedValue('hashed_password');
|
||||
mockUserRepository.create.mockResolvedValue({
|
||||
id: '1',
|
||||
email: 'test@example.com',
|
||||
password: 'hashed_password',
|
||||
createdAt: new Date()
|
||||
});
|
||||
|
||||
const user = await service.register('test@example.com', 'password123');
|
||||
|
||||
expect(user.id).toBe('1');
|
||||
expect(mockPasswordHasher.hash).toHaveBeenCalledWith('password123');
|
||||
expect(mockEmailService.send).toHaveBeenCalled();
|
||||
expect(mockAnalytics.track).toHaveBeenCalledWith('user_registered', { userId: '1' });
|
||||
});
|
||||
|
||||
it('should throw error if user already exists', async () => {
|
||||
mockUserRepository.findByEmail.mockResolvedValue({ id: '1', email: 'test@example.com' } as User);
|
||||
|
||||
await expect(service.register('test@example.com', 'password123'))
|
||||
.rejects.toThrow(UserAlreadyExistsError);
|
||||
|
||||
expect(mockUserRepository.create).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not fail registration if email sending fails', async () => {
|
||||
mockUserRepository.findByEmail.mockResolvedValue(null);
|
||||
mockPasswordHasher.hash.mockResolvedValue('hashed_password');
|
||||
mockUserRepository.create.mockResolvedValue({ id: '1', email: 'test@example.com' } as User);
|
||||
mockEmailService.send.mockRejectedValue(new Error('SMTP error'));
|
||||
|
||||
const user = await service.register('test@example.com', 'password123');
|
||||
|
||||
expect(user.id).toBe('1');
|
||||
expect(mockLogger.error).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Why
|
||||
|
||||
1. **Testability**: All dependencies can be mocked. Tests run fast without external services.
|
||||
|
||||
2. **Flexibility**: Switch implementations at configuration time, not code change time.
|
||||
|
||||
3. **Single Responsibility**: Classes focus on business logic, not on constructing dependencies.
|
||||
|
||||
4. **Configuration Centralization**: All wiring happens in one place (composition root), making it easy to see and change.
|
||||
|
||||
5. **Lifetime Management**: The container can manage singleton vs. transient instances.
|
||||
|
||||
6. **Environment Adaptation**: Easy to provide different implementations for dev, test, staging, and production.
|
||||
|
||||
7. **Explicit Dependencies**: Looking at the constructor tells you exactly what a class needs to function.
|
||||
Reference in New Issue
Block a user