Sobes.tech
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:

  1. Moduły wyższego poziomu nie powinny zależeć od modułów niższego poziomu. Oba powinny zależeć od abstrakcji.
  2. 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.