Τι είναι, γιατί υπάρχει και πώς λειτουργούν τα Lifetimes
Η σύγχρονη ανάπτυξη λογισμικού δεν αφορά μόνο αλγορίθμους και δομές δεδομένων. Αφορά κυρίως τη δομή του συστήματος: πώς συνεργάζονται τα αντικείμενα, πώς εξαρτώνται μεταξύ τους, πώς ελέγχουμε τη διάρκεια ζωής τους και πώς διατηρούμε τον κώδικα καθαρό, επεκτάσιμο και ελέγξιμο.
Το Dependency Injection (DI) είναι ένας από τους βασικούς μηχανισμούς που μας επιτρέπει να το πετύχουμε αυτό.
Τι είναι το Dependency Injection
Το πρόβλημα χωρίς DI
Φανταστείτε μια κλάση που δημιουργεί μόνη της τις εξαρτήσεις της:
public class OrdersService
{
private readonly OrdersRepository _repository;
public OrdersService()
{
_repository = new OrdersRepository();
}
}
Αυτό δημιουργεί:
- Ισχυρή σύζευξη (tight coupling)
- Δυσκολία στα unit tests
- Δυσκολία στην αλλαγή υλοποιήσεων
- Κρυμμένες εξαρτήσεις
Η κλάση δεν δηλώνει τι χρειάζεται. Το “χτίζει” μόνη της.
Η φιλοσοφία του DI
Το Dependency Injection λέει:
Μην δημιουργείς τις εξαρτήσεις σου. Δήλωσε τι χρειάζεσαι και άφησε κάποιον άλλο να στις παρέχει.
public class OrdersService
{
private readonly IOrdersRepository _repository;
public OrdersService(IOrdersRepository repository)
{
_repository = repository;
}
}
Τώρα:
- Η κλάση είναι πιο καθαρή
- Μπορούμε να κάνουμε inject mock
- Η ευθύνη της δημιουργίας αντικειμένων μεταφέρεται στο DI container
Στο .NET αυτό γίνεται μέσω:
services.AddScoped<IOrdersRepository, OrdersRepository>();
Το κρίσιμο σημείο: Τα Lifetimes
Το πιο σημαντικό κομμάτι στο DI δεν είναι απλώς το injection. Είναι το lifetime δηλαδή:
Πόσο ζει ένα αντικείμενο;
Στο .NET έχουμε τρεις βασικούς τύπους διάρκειας ζωής:
- Transient
- Scoped
- Singleton
Ας τους δούμε αναλυτικά.
Transient Κάθε φορά καινούριο
services.AddTransient<IMyService, MyService>();
Τι σημαίνει
Κάθε φορά που ζητείται το service, δημιουργείται νέο instance.
Αν μέσα στο ίδιο HTTP request:
- 3 διαφορετικές κλάσεις ζητήσουν το ίδιο service,
- θα δημιουργηθούν 3 διαφορετικά αντικείμενα.
Πότε το χρησιμοποιούμε
- Stateless helpers
- Mappers
- Validators
- Lightweight services
- Λειτουργίες χωρίς shared state
Πλεονεκτήματα
- Δεν μοιράζεται state
- Δεν έχει side effects μεταξύ consumers
- Ασφαλές από concurrency bugs
Μειονεκτήματα
- Αν είναι “βαρύ” αντικείμενο, δημιουργείται πολλές φορές
- Μπορεί να έχει performance κόστος
Scoped Ένα ανά Scope (συνήθως ανά HTTP Request)
Τι σημαίνει
Ένα instance δημιουργείται ανά scope.
Στο ASP.NET Core:
- Το scope είναι συνήθως το HTTP request.
- Όλοι μέσα στο ίδιο request μοιράζονται το ίδιο instance.
Τυπικό παράδειγμα
DbContext
Γιατί;
- Θέλουμε tracking αλλαγών μέσα στο ίδιο request
- Θέλουμε κοινό unit of work
- Θέλουμε dispose στο τέλος του request
Πότε το χρησιμοποιούμε
- Repositories
- Domain services που δουλεύουν με DbContext
- Services που χρειάζονται request-level state
- Context services (π.χ. CurrentUser)
Πλεονεκτήματα
- Shared state εντός request
- Σωστό lifecycle management
- Αυτόματο dispose στο τέλος
Μειονεκτήματα
- Δεν είναι global
- Δεν μπορεί να χρησιμοποιηθεί απευθείας μέσα σε Singleton
Singleton — Ένα για όλη την εφαρμογή
Τι σημαίνει
Το αντικείμενο δημιουργείται μία φορά και ζει όσο ζει η εφαρμογή.
Πότε το χρησιμοποιούμε
- Configuration services
- In-memory caching
- Stateless utilities
- Expensive-to-create objects που είναι thread-safe
Προϋπόθεση
Πρέπει να είναι thread-safe.
Γιατί;
Το ίδιο instance θα χρησιμοποιείται από πολλά requests ταυτόχρονα.
Μειονεκτήματα
- Shared global state
- Πιθανότητα concurrency bugs
- Κίνδυνος memory leaks αν κρατά request data
Η πιο επικίνδυνη παγίδα: Captive Dependency
Το πιο ύπουλο λάθος στο DI:
Singleton που inject-άρει Scoped service.
Παράδειγμα:
services.AddSingleton<MySingletonService>();
services.AddScoped<MyDbContext>();
Και μέσα στο Singleton:
public MySingletonService(MyDbContext dbContext)
Τι συμβαίνει;
- Το scoped αντικείμενο δημιουργείται μία φορά.
- Μένει μέσα στο singleton.
- Δεν γίνεται dispose ανά request.
- Συμπεριφέρεται σαν singleton.
Αυτό μπορεί να οδηγήσει σε:
- Memory leaks
- Concurrency προβλήματα
- Χρήση λάθος user context
- DbContext corruption
Να θυμάσαι..
Όταν δηλώνουμε μια υπηρεσία ως Singleton και της κάνουμε inject ένα Scoped dependency, δημιουργούμε μια από τις πιο ύπουλες παγίδες στο Dependency Injection. Στο παράδειγμα όπου το MySingletonService είναι singleton και δέχεται στο constructor ένα MyDbContext που είναι scoped, το DI container θα δημιουργήσει το MyDbContext τη στιγμή που θα δημιουργηθεί το singleton. Επειδή όμως το singleton ζει για όλη τη διάρκεια της εφαρμογής, το ίδιο instance του MyDbContext θα παραμείνει δεσμευμένο μέσα του και θα χρησιμοποιείται σε όλα τα requests. Αυτό σημαίνει ότι το scoped αντικείμενο δεν θα δημιουργείται πλέον ανά request όπως σχεδιάστηκε, ούτε θα γίνεται dispose στο τέλος κάθε request. Ουσιαστικά «αιχμαλωτίζεται» μέσα στο singleton και συμπεριφέρεται σαν να ήταν και το ίδιο singleton. Σε πραγματικές συνθήκες αυτό μπορεί να οδηγήσει σε σοβαρά προβλήματα: το DbContext δεν είναι thread-safe, άρα μπορεί να προκύψουν concurrency σφάλματα· μπορεί να κρατά δεδομένα προηγούμενου χρήστη και να τα εκθέτει σε επόμενο request· μπορεί να συσσωρεύει tracked entities και να αυξάνει συνεχώς τη μνήμη· και επειδή δεν αποδεσμεύεται σωστά, να δημιουργούνται resource leaks ή ακόμα και corruption στην κατάσταση του context. Το αποτέλεσμα είναι ένα σύστημα που λειτουργεί «φαινομενικά» σωστά στην αρχή αλλά αποτυγχάνει υπό φόρτο ή μετά από χρόνο λειτουργίας — δηλαδή το πιο δύσκολο είδος bug για διάγνωση.
Application Start
│
▼
Create Singleton
│
├──> Creates Scoped DbContext (μόνο μία φορά)
│
▼
┌───────────────────────────┐
│ MySingletonService │
│ ───────────────────── │
│ holds MyDbContext │ ◄──── ίδιο instance
└───────────────────────────┘
│
│
├── HTTP Request 1 ── uses same DbContext
│
├── HTTP Request 2 ── uses same DbContext
│
├── HTTP Request 3 ── uses same DbContext
│
▼
Application Shutdown (dispose happens only here)
Ο Χρυσός Κανόνας
Ποτέ μην inject-άρεις μικρότερης διάρκειας lifetime σε μεγαλύτερης διάρκειας service.
Ιεραρχία:
Singleton
↑
Scoped
↑
Transient
Δεν επιτρέπεται:
Singleton → Scoped ❌
Singleton → Transient που εσωτερικά κρατά Scoped ❌
Πώς επιλέγω το σωστό Lifetime;
Ακολουθήστε αυτόν τον πρακτικό αλγόριθμο:
Κρατάει state;
- Όχι → Transient ή Singleton
- Ναι, ανά request → Scoped
- Ναι, global → Singleton
Χρησιμοποιεί DbContext;
Ναι → Scoped (σχεδόν πάντα)
Είναι thread-safe;
Όχι → ΜΗΝ το κάνεις Singleton
Πρακτική αντιστοίχιση στην πραγματικότητα
| Τύπος Service | Lifetime |
| ---------------------- | --------- |
| DbContext | Scoped |
| Repository | Scoped |
| Domain Service | Scoped |
| Validator | Transient |
| Mapper | Transient |
| Cache Service | Singleton |
| Configuration Provider | Singleton |
| Background Scheduler | Singleton |
Τελικό Συμπέρασμα
Το Dependency Injection δεν είναι απλώς ένα pattern. Είναι αρχιτεκτονική επιλογή.
Η σωστή χρήση των lifetimes:
- Βελτιώνει τη σταθερότητα
- Αποτρέπει memory leaks
- Εξασφαλίζει σωστό isolation ανά request
- Αποτρέπει concurrency bugs
- Κάνει τον κώδικα testable και επεκτάσιμο
Η λάθος επιλογή lifetime δεν φαίνεται πάντα αμέσως.
Συχνά οδηγεί σε “σιωπηλά” bugs που εμφανίζονται σε παραγωγή.
Να θυμάσαι
Στην πράξη, η επιλογή του σωστού lifetime δεν είναι μια τεχνική λεπτομέρεια που αφορά μόνο τον προγραμματιστή. Είναι μια επιχειρησιακή απόφαση που επηρεάζει τη σταθερότητα, την απόδοση και τη σωστή συμπεριφορά της εφαρμογής. Κάθε lifetime εκφράζει μια διαφορετική «διάρκεια ζωής ευθύνης» μέσα στο σύστημα.
Το Transient χρησιμοποιείται όταν μια υπηρεσία δεν κρατά καμία μνήμη και δεν χρειάζεται να μοιράζεται κατάσταση με κανέναν. Είναι κατάλληλο για καθαρές λειτουργίες, βοηθητικές υπηρεσίες, μετατροπές ή ελέγχους που εκτελούνται και ολοκληρώνονται χωρίς να επηρεάζουν το υπόλοιπο σύστημα. Κάθε χρήση δημιουργεί ένα νέο αντικείμενο και αυτό προσφέρει απομόνωση και ασφάλεια, ιδιαίτερα όταν δεν θέλουμε ανεπιθύμητες παρενέργειες.
Το Scoped lifetime εκφράζει την ιδέα ότι μια υπηρεσία ανήκει σε μια συγκεκριμένη πράξη. Στον κόσμο των web εφαρμογών, αυτή η πράξη είναι συνήθως ένα HTTP request. Ό,τι συμβαίνει μέσα σε αυτό το αίτημα μοιράζεται την ίδια κατάσταση και ολοκληρώνεται μαζί του. Είναι η σωστή επιλογή όταν υπάρχει επιχειρησιακή λογική που αφορά έναν συγκεκριμένο χρήστη ή μια συγκεκριμένη συναλλαγή. Με αυτόν τον τρόπο διασφαλίζεται ότι τα δεδομένα δεν διαρρέουν από αίτημα σε αίτημα και ότι οι πόροι αποδεσμεύονται σωστά στο τέλος της λειτουργίας.
Το Singleton, αντίθετα, αντιπροσωπεύει κάτι που ανήκει στην ίδια την εφαρμογή και όχι σε μια συγκεκριμένη ενέργεια. Δημιουργείται μία φορά και παραμένει ενεργό όσο λειτουργεί το σύστημα. Είναι κατάλληλο για υποδομές όπως caching, configuration ή κοινές υπηρεσίες που πρέπει να είναι διαθέσιμες παντού και είναι ασφαλείς για ταυτόχρονη χρήση. Εδώ απαιτείται ιδιαίτερη προσοχή, γιατί οτιδήποτε μοιράζεται σε ολόκληρη την εφαρμογή πρέπει να είναι σχεδιασμένο με σκέψη ως προς την ασφάλεια και την ταυτόχρονη πρόσβαση.
Η ουσία είναι απλή. Αν κάτι ανήκει σε μια συγκεκριμένη ενέργεια, είναι Scoped. Αν ανήκει σε ολόκληρη την εφαρμογή, είναι Singleton. Αν δεν κρατά καθόλου κατάσταση και δεν χρειάζεται να μοιράζεται δεδομένα, είναι Transient. Η σωστή επιλογή δεν βασίζεται στη συνήθεια, αλλά στη λογική κατανόηση του ρόλου που παίζει κάθε υπηρεσία μέσα στο σύστημα.
Το Dependency Injection δεν είναι απλώς ένας τρόπος να αποφεύγουμε τη λέξη “new”. Είναι ένας τρόπος να δηλώνουμε με σαφήνεια πόσο ζουν τα αντικείμενα και σε ποιο επίπεδο ευθύνης ανήκουν. Και αυτή η σαφήνεια είναι που κάνει ένα σύστημα πραγματικά αξιόπιστο.

Top comments (0)