Sobes.tech
Senior

Как да решим проблема с използването на зависимости на периферните компоненти в ядрото?

sobes.tech AI

Отговор от AI

Решаването на проблема с използването на зависимости на периферните компоненти (адаптери, интерфейси) в ядрото (домейн логиката) в архитектури като Чистата архитектура или DDD, може да се постигне чрез принципа Инверсия на зависимостите (Dependency Inversion Principle - DIP).

Същността на DIP:

  1. Модулите от по-високо ниво не трябва да зависят от модулите от по-ниско ниво. И двата трябва да зависят от абстракции.
  2. Абстракциите не трябва да зависят от детайлите. Детайлите трябва да зависят от абстракциите.

Практическо приложение:

  • Ядрото обявява интерфейси: Ядрото определя абстракции (интерфейси или абстрактни класове), които описват необходимите му функции от периферните компоненти.
  • Периферните компоненти реализират интерфейсите: Външните модули (адаптери за бази данни, HTTP клиенти, файлови системи и т.н.) реализират тези интерфейси.
  • Внедряване на зависимости (Dependency Injection - DI): Периферните компоненти се внедряват в ядрото отвън, обикновено на по-високо ниво на приложението (например в композиционния корен или с използване на DI контейнер). Ядрото работи само с абстракции.

Пример за използване на DIP в Node.js:

Да предположим, че имаме ядро, което се нуждае от достъп до данни.

// src/domain/interfaces/UserRepository.ts
// Ядрото обявява интерфейс
export interface UserRepository {
  getUserById(id: string): Promise<User | null>;
}
// src/domain/entities/User.ts
// Ядро същност
export class User {
  constructor(public id: string, public name: string) {}
}
// src/domain/usecases/GetUserUseCase.ts
// Use case на ядрото зависи от абстракцията 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
// Периферен компонент, който реализира интерфейса на ядрото
import { UserRepository } from '../../domain/interfaces/UserRepository';
import { User } from '../../domain/entities/User';

export class MongoDbUserRepository implements UserRepository {
  // Тук ще бъде логиката за взаимодействие с MongoDB

  async getUserById(id: string): Promise<User | null> {
    console.log(`Извличане на потребител с id ${id} от MongoDB...`);
    // Имитация на получаване на данни
    if (id === '123') {
      return new User('123', 'John Doe');
    }
    return null;
  }
}
// src/app/CompositionRoot.ts
// Композиционният корен събира зависимостите
import { GetUserUseCase } from '../domain/usecases/GetUserUseCase';
import { MongoDbUserRepository } from '../infrastructure/database/MongoDbUserRepository';

// Внедряване на зависимост: Ядрото получава реализацията MongoDbUserRepository чрез интерфейса
const userRepository = new MongoDbUserRepository();
const getUserUseCase = new GetUserUseCase(userRepository);

// Сега можем да използваме use case
async function run() {
  const user = await getUserUseCase.execute('123');
  if (user) {
    console.log(`Намерен потребител: ${user.name}`);
  } else {
    console.log('Потребител не е намерен');
  }
}

run();

Предимствата на този подход:

  • Независимост на ядрото: Ядрото не знае за конкретните реализации на периферните компоненти.
  • Тестируемост: Ядрото е лесно за тестване, като се използват мок-реализации на интерфейсите.
  • Гъвкавост: Лесно може да се замени една реализация (например, база данни) с друга, без да се променя кодът на ядрото.
  • Поддръжка: Улеснява поддръжката и промяната на кода.