Dependency Injection Erklaert
Dependency Injection: Besserer Code durch lose Kopplung
Dependency Injection macht Code testbar und wartbar. Lernen Sie das Konzept und wie Sie es anwenden.
Das Problem
// ❌ Tight Coupling - schwer zu testen
class OrderService {
constructor() {
this.db = new Database(); // Hartcodiert!
this.mailer = new EmailService(); // Hartcodiert!
this.logger = new Logger(); // Hartcodiert!
}
async createOrder(orderData) {
const order = await this.db.save(orderData);
await this.mailer.send(order.userEmail, 'Order confirmed');
this.logger.info('Order created', order.id);
return order;
}
}
// Probleme:
// - Kann nicht mit Mock-DB testen
// - Kann Email-Versand nicht mocken
// - Änderung der DB erfordert Code-Änderung
Die Lösung: Dependency Injection
// ✅ Dependencies werden injiziert
class OrderService {
constructor(db, mailer, logger) {
this.db = db;
this.mailer = mailer;
this.logger = logger;
}
async createOrder(orderData) {
const order = await this.db.save(orderData);
await this.mailer.send(order.userEmail, 'Order confirmed');
this.logger.info('Order created', order.id);
return order;
}
}
// Production
const orderService = new OrderService(
new PostgresDatabase(),
new SendGridMailer(),
new WinstonLogger()
);
// Test - mit Mocks!
const orderService = new OrderService(
mockDatabase,
mockMailer,
mockLogger
);
DI-Arten
// 1. Constructor Injection (empfohlen)
class UserService {
constructor(repository) {
this.repository = repository;
}
}
// 2. Setter Injection
class UserService {
setRepository(repository) {
this.repository = repository;
}
}
// 3. Interface Injection
class UserService {
injectDependencies(container) {
this.repository = container.get('UserRepository');
}
}
TypeScript mit Interfaces
// Interfaces definieren den Vertrag
interface IUserRepository {
findById(id: string): Promise<User | null>;
save(user: User): Promise<User>;
}
interface IEmailService {
send(to: string, subject: string, body: string): Promise<void>;
}
// Implementierungen
class PostgresUserRepository implements IUserRepository {
async findById(id: string) { /* ... */ }
async save(user: User) { /* ... */ }
}
class MockUserRepository implements IUserRepository {
users = new Map<string, User>();
async findById(id: string) {
return this.users.get(id) || null;
}
async save(user: User) {
this.users.set(user.id, user);
return user;
}
}
// Service nutzt Interface
class UserService {
constructor(
private repository: IUserRepository,
private emailService: IEmailService
) {}
async createUser(data: CreateUserDTO) {
const user = await this.repository.save(new User(data));
await this.emailService.send(user.email, 'Welcome', '...');
return user;
}
}
DI Container (InversifyJS)
npm install inversify reflect-metadata
import { Container, injectable, inject } from 'inversify';
import 'reflect-metadata';
// Symbols für DI
const TYPES = {
UserRepository: Symbol.for('UserRepository'),
EmailService: Symbol.for('EmailService'),
UserService: Symbol.for('UserService'),
};
@injectable()
class PostgresUserRepository implements IUserRepository {
// ...
}
@injectable()
class UserService {
constructor(
@inject(TYPES.UserRepository) private repo: IUserRepository,
@inject(TYPES.EmailService) private email: IEmailService
) {}
}
// Container konfigurieren
const container = new Container();
container.bind<IUserRepository>(TYPES.UserRepository).to(PostgresUserRepository);
container.bind<IEmailService>(TYPES.EmailService).to(SendGridService);
container.bind<UserService>(TYPES.UserService).to(UserService);
// Verwendung
const userService = container.get<UserService>(TYPES.UserService);
PHP: Constructor Injection
<?php
interface UserRepositoryInterface {
public function find(int $id): ?User;
public function save(User $user): User;
}
class UserService {
public function __construct(
private UserRepositoryInterface $repository,
private MailerInterface $mailer,
private LoggerInterface $logger
) {}
public function createUser(array $data): User {
$user = new User($data);
$this->repository->save($user);
$this->mailer->send($user->email, 'Welcome');
$this->logger->info('User created', ['id' => $user->id]);
return $user;
}
}
// Mit Laravel (automatische Injection)
// App\Providers\AppServiceProvider
public function register(): void
{
$this->app->bind(UserRepositoryInterface::class, EloquentUserRepository::class);
$this->app->bind(MailerInterface::class, SendGridMailer::class);
}
// Controller - automatisch injiziert
class UserController {
public function __construct(
private UserService $userService
) {}
}
Testen mit DI
// Jest Test
describe('UserService', () => {
let userService: UserService;
let mockRepo: jest.Mocked<IUserRepository>;
let mockEmail: jest.Mocked<IEmailService>;
beforeEach(() => {
mockRepo = {
findById: jest.fn(),
save: jest.fn(),
};
mockEmail = {
send: jest.fn(),
};
userService = new UserService(mockRepo, mockEmail);
});
test('createUser saves and sends email', async () => {
const userData = { name: 'Max', email: 'max@test.com' };
mockRepo.save.mockResolvedValue({ id: '1', ...userData });
mockEmail.send.mockResolvedValue(undefined);
const result = await userService.createUser(userData);
expect(mockRepo.save).toHaveBeenCalledWith(expect.any(User));
expect(mockEmail.send).toHaveBeenCalledWith('max@test.com', 'Welcome', expect.any(String));
expect(result.id).toBe('1');
});
});
💡 SOLID Prinzip:
DI unterstützt das Dependency Inversion Principle: Abhänge von Abstraktionen (Interfaces), nicht von konkreten Implementierungen.