Senior
Jak rozwiązać problem korzystania z zależności komponentów peryferyjnych w jądrze?
sobes.tech AI
Odpowiedź od AI
Rozwiązanie problemu korzystania z zależności komponentów peryferyjnych (adapterów, interfejsów) w jądrze (logice domeny) w architekturze, takiej jak Czysta Architektura lub DDD, można osiągnąć za pomocą zasady odwrócenia zależności (Dependency Inversion Principle - DIP).
Istota DIP:
- Moduły wyższego poziomu nie powinny zależeć od modułów niższego poziomu. Oba powinny zależeć od abstrakcji.
- Abstrakcje nie powinny zależeć od szczegółów. Szczegóły powinny zależeć od abstrakcji.
Praktyczne zastosowanie:
- Jądro deklaruje interfejsy: Jądro definiuje abstrakcje (interfejsy lub klasy abstrakcyjne), które opisują niezbędne funkcje komponentów peryferyjnych.
- Komponenty peryferyjne implementują interfejsy: Zewnętrzne moduły (adaptery do baz danych, klientów HTTP, systemów plików itp.) implementują te interfejsy.
- Wstrzykiwanie zależności (Dependency Injection - DI): Komponenty peryferyjne są wstrzykiwane do jądra z zewnątrz, zwykle na wyższym poziomie aplikacji (np. w korzeniu kompozycji lub za pomocą kontenera DI). Jądro działa tylko z abstrakcjami.
Przykład użycia DIP w Node.js:
Załóżmy, że mamy jądro, które potrzebuje dostępu do danych.
// src/domain/interfaces/UserRepository.ts
// Jądro deklaruje interfejs
export interface UserRepository {
getUserById(id: string): Promise<User | null>;
}
// src/domain/entities/User.ts
// Encja jądra
export class User {
constructor(public id: string, public name: string) {}
}
// src/domain/usecases/GetUserUseCase.ts
// Użycie jądra zależy od abstrakcji 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
// Komponent peryferyjny implementujący interfejs jądra
import { UserRepository } from '../../domain/interfaces/UserRepository';
import { User } from '../../domain/entities/User';
export class MongoDbUserRepository implements UserRepository {
// Logika interakcji z MongoDB
async getUserById(id: string): Promise<User | null> {
console.log(`Fetching user with id ${id} from MongoDB...`);
// Symulacja pobierania danych
if (id === '123') {
return new User('123', 'John Doe');
}
return null;
}
}
// src/app/CompositionRoot.ts
// Korzeń kompozycji zbiera zależności
import { GetUserUseCase } from '../domain/usecases/GetUserUseCase';
import { MongoDbUserRepository } from '../infrastructure/database/MongoDbUserRepository';
// Wstrzykiwanie zależności: Jądro otrzymuje implementację MongoDbUserRepository przez interfejs
const userRepository = new MongoDbUserRepository();
const getUserUseCase = new GetUserUseCase(userRepository);
// Teraz możemy użyć przypadku użycia
async function run() {
const user = await getUserUseCase.execute('123');
if (user) {
console.log(`Found user: ${user.name}`);
} else {
console.log('User not found');
}
}
run();
Zalety tego podejścia:
- Niezależność jądra: Jądro nie zna konkretnych implementacji komponentów peryferyjnych.
- Testowalność: Jądro jest łatwe do testowania, podstawiając mocki interfejsów.
- Elastyczność: Można łatwo wymienić jedną implementację (np. bazę danych) na inną, nie zmieniając kodu jądra.
- Utrzymanie: Ułatwia to konserwację i modyfikację kodu.