Sobes.tech
Senior

Πώς λύνετε το πρόβλημα της χρήσης εξαρτήσεων των περιφερειακών συστατικών στον πυρήνα;

sobes.tech AI

Απάντηση από AI

Η επίλυση του προβλήματος χρήσης των εξαρτήσεων των περιφερειακών συστατικών (ανταπτόρων, διεπαφών) στον πυρήνα (λογική τομέα) σε μια αρχιτεκτονική, όπως η Καθαρή Αρχιτεκτονική ή το DDD, μπορεί να επιτευχθεί με την αρχή Αντιστροφής Εξαρτήσεων (Dependency Inversion Principle - DIP).

Η ουσία του DIP:

  1. Τα ανώτερα επίπεδα modules δεν πρέπει να εξαρτώνται από τα κατώτερα επίπεδα. Και τα δύο πρέπει να εξαρτώνται από αφαιρέσεις.
  2. Οι αφαιρέσεις δεν πρέπει να εξαρτώνται από λεπτομέρειες. Οι λεπτομέρειες πρέπει να εξαρτώνται από τις αφαιρέσεις.

Πρακτική εφαρμογή:

  • Ο πυρήνας δηλώνει διεπαφές: Ο πυρήνας ορίζει αφαιρέσεις (διεπαφές ή αφηρημένες κλάσεις) που περιγράφουν τις απαραίτητες λειτουργίες από τα περιφερειακά συστατικά.
  • Τα περιφερειακά συστατικά υλοποιούν τις διεπαφές: Εξωτερικά modules (ανταπτόρες για βάσεις δεδομένων, HTTP clients, συστήματα αρχείων κ.λπ.) υλοποιούν αυτές τις διεπαφές.
  • Εισαγωγή εξαρτήσεων (Dependency Injection - DI): Τα περιφερειακά συστατικά εισάγονται στον πυρήνα από έξω, συνήθως σε υψηλότερο επίπεδο της εφαρμογής (π.χ., στον πυρήνα σύνθεσης ή με τη χρήση DI-container). Ο πυρήνας λειτουργεί μόνο με αφαιρέσεις.

Παράδειγμα χρήσης 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
// Η χρήση πυρήνα εξαρτάται από την αφαιρέση 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);

// Τώρα μπορούμε να χρησιμοποιήσουμε την περίπτωση χρήσης
async function run() {
  const user = await getUserUseCase.execute('123');
  if (user) {
    console.log(`Βρέθηκε χρήστης: ${user.name}`);
  } else {
    console.log('Χρήστης δεν βρέθηκε');
  }
}

run();

Πλεονεκτήματα αυτής της προσέγγισης:

  • Ανεξαρτησία πυρήνα: Ο πυρήνας δεν γνωρίζει για τις συγκεκριμένες υλοποιήσεις των περιφερειακών συστατικών.
  • Δοκιμαστικότητα: Ο πυρήνας είναι εύκολο να δοκιμαστεί, αντικαθιστώντας τις mock-υλοποιήσεις διεπαφών.
  • Ευελιξία: Μπορεί εύκολα να αντικαταστήσει μια υλοποίηση (π.χ., βάση δεδομένων) με άλλη, χωρίς να αλλάξει ο κώδικας του πυρήνα.
  • Υποστήριξη: Απλοποιείται η συντήρηση και η τροποποίηση του κώδικα.