Senior
Hoe los je het probleem op van het gebruik van afhankelijkheden van perifere componenten in de kernel?
sobes.tech AI
Antwoord van AI
Oplossen van het probleem van het gebruik van afhankelijkheden van perifere componenten (adapters, interfaces) in de kern (domeinlogica) in architecturen zoals de Schone Architectuur of DDD, kan worden gedaan met behulp van het principe van Dependency Inversion Principle - DIP.
De kern van DIP:
- Modules van hoger niveau mogen niet afhankelijk zijn van modules van lager niveau. Beide moeten afhankelijk zijn van abstracties.
- Abstracties mogen niet afhankelijk zijn van details. Details moeten afhankelijk zijn van abstracties.
Praktische toepassing:
- De kern declareert interfaces: De kern definieert abstracties (interfaces of abstracte klassen) die de benodigde functies van perifere componenten beschrijven.
- Perifere componenten implementeren interfaces: Externe modules (adapters voor databases, HTTP-clients, bestandssystemen, etc.) implementeren deze interfaces.
- Dependency Injection (DI): Perifere componenten worden van buitenaf in de kern geïnjecteerd, meestal op een hoger niveau van de applicatie (bijvoorbeeld in de compositiekern of met behulp van een DI-container). De kern werkt alleen met abstracties.
Voorbeeld van DIP in Node.js:
Stel dat we een kern hebben die toegang tot gegevens nodig heeft.
// src/domain/interfaces/UserRepository.ts
// De kern declareert de interface
export interface UserRepository {
getUserById(id: string): Promise<User | null>;
}
// src/domain/entities/User.ts
// Entiteit van de kern
export class User {
constructor(public id: string, public name: string) {}
}
// src/domain/usecases/GetUserUseCase.ts
// Use case van de kern afhankelijk van de abstractie UserRepository
import { UserRepository } from '../interfaces/UserRepository';
import { User } from '../entities/User';
export class GetUserUseCase {
constructor(private userRepository: UserRepository) {}
async execute(id: string): Promise<User | null> {
return this.userRepository.getUserById(id);
}
}
// src/infrastructure/database/MongoDbUserRepository.ts
// Perifere component die de interface van de kern implementeert
import { UserRepository } from '../../domain/interfaces/UserRepository';
import { User } from '../../domain/entities/User';
export class MongoDbUserRepository implements UserRepository {
// Hier komt de logica voor interactie met MongoDB
async getUserById(id: string): Promise<User | null> {
console.log(`Fetching user with id ${id} from MongoDB...`);
// Simulatie van gegevens ophalen
if (id === '123') {
return new User('123', 'John Doe');
}
return null;
}
}
// src/app/CompositionRoot.ts
// De compositiewortel verzamelt afhankelijkheden
import { GetUserUseCase } from '../domain/usecases/GetUserUseCase';
import { MongoDbUserRepository } from '../infrastructure/database/MongoDbUserRepository';
// Dependency injection: De kern krijgt de implementatie MongoDbUserRepository via de interface
const userRepository = new MongoDbUserRepository();
const getUserUseCase = new GetUserUseCase(userRepository);
// Nu kunnen we de use case gebruiken
async function run() {
const user = await getUserUseCase.execute('123');
if (user) {
console.log(`Found user: ${user.name}`);
} else {
console.log('User not found');
}
}
run();
Voordelen van deze aanpak:
- Onafhankelijkheid van de kern: De kern kent geen specifieke implementaties van perifere componenten.
- Testbaarheid: De kern is gemakkelijk te testen door mock-implementaties van de interfaces te gebruiken.
- Flexibiliteit: Het is eenvoudig om één implementatie (bijvoorbeeld database) te vervangen door een andere, zonder de kerncode te wijzigen.
- Onderhoud: Het onderhoud en de wijziging van de code wordt vergemakkelijkt.