Service Locator — η ουσία του προτύπου, κεντρικό μητρώο και DI

Συγγραφέας: IT Sectr Δημοσιεύτηκε: 2026-02-18 Χρόνος ανάγνωσης: 9 λεπ

Service Locator — ένα αρχιτεκτονικό πρότυπο που παρέχει ένα κεντρικό μητρώο (registry) υπηρεσιών. Ο κώδικας πελάτη ζητά μια υπηρεσία μέσω ενός στατικού εντοπιστή, χωρίς να τη δημιουργεί άμεσα ή να τη λαμβάνει μέσω κατασκευαστή. Το Service Locator συχνά θεωρείται εναλλακτική του Dependency Injection: είναι απλούστερο στην υλοποίηση, αλλά κρύβει εξαρτήσεις και δυσκολεύει τον έλεγχο. Το πρότυπο υλοποιείται μέσω μιας καθολικής κλάσης Singleton με καταχώρηση και επίλυση υπηρεσιών. Περισσότερα — στη σύγκριση DI και Service Locator από τον Martin Fowler.

Κύρια σημεία

  • Service Locator — κεντρικό μητρώο (registry) για λήψη υπηρεσιών
  • Εναλλακτική DI — απλούστερο στην υλοποίηση, αλλά κρύβει εξαρτήσεις από τον πελάτη
  • Αντί-πρότυπο — πολλοί προγραμματιστές θεωρούν το Service Locator αντί-πρότυπο λόγω κρυφών εξαρτήσεων
  • Καθολική πρόσβαση — ο εντοπιστής είναι διαθέσιμος στατικά από οπουδήποτε, χωρίς μεταβίβαση μέσω κατασκευαστή
  • Έλεγχος — δυσκολότερος από το DI: απαιτεί ρύθμιση του καθολικού εντοπιστή για κάθε δοκιμή

Τι είναι το Service Locator: ουσία και δομή του προτύπου

Service Locator — ένα πρότυπο που συγκεντροποιεί τη δημιουργία και παροχή υπηρεσιών. Στη βάση βρίσκεται μια κλάση Singleton (Locator) που περιέχει ένα μητρώο (Registry) υπηρεσιών: ένα λεξικό όπου το κλειδί είναι ο τύπος (ή το αναγνωριστικό) της υπηρεσίας και η τιμή είναι η συγκεκριμένη υλοποίηση. Ο κώδικας πελάτη καλεί ServiceLocator.resolve(ServiceProtocol.self) και λαμβάνει ένα έτοιμο στιγμιότυπο. Το πρότυπο δεν ορίζει πώς δημιουργείται η υπηρεσία — εργοστάσιο, κοντέινερ DI ή new μέσα στον εντοπιστή.

Δομή του προτύπου — Registry (μητρώο τύπου [String: Any]), Locator (στατική κλάση με register και resolve), Service (καταχωρήσιμη υπηρεσία). Το μητρώο μπορεί να αποθηκεύει εργοστάσια (closure/lambda για δημιουργία αντικειμένου) ή έτοιμα στιγμιότυπα. Ο εντοπιστής μπορεί να είναι καθολικός (ένας ανά εφαρμογή) ή με εμβέλεια (ανά feature/module). Η επίλυση υπηρεσίας είναι μια αναζήτηση στο λεξικό ανά τύπο. Οι Swift και Kotlin χρησιμοποιούν τον τύπο ως κλειδί μέσω μετατύπων: ObjectIdentifier(ServiceProtocol.self).

ΣτοιχείοΕυθύνηSwift/Kotlin
ServiceLocatorΚαθολική πρόσβαση σε υπηρεσίεςclass ServiceLocator
RegistryΑποθήκη εργοστασίων/στιγμιοτύπων[ObjectIdentifier: Any]
ServiceΣυγκεκριμένη υλοποίησηNetworkService()

Ιστορία του προτύπου — Το Service Locator περιγράφηκε στο βιβλίο Java Patterns (1998) και αργότερα στο Core J2EE Patterns (2001). Ο Martin Fowler σε άρθρο του 2004 συγκρίνει το Service Locator με το DI, σημειώνοντας ότι το Service Locator είναι «μια απλούστερη εναλλακτική, αλλά χειρότερη για έλεγχο». Στην κινητή ανάπτυξη, το Service Locator χρησιμοποιούνταν σε πρώιμα έργα Android και εφαρμογές iOS πριν από την εμφάνιση των Dagger και Swinject. Τώρα το πρότυπο συναντάται συχνότερα σε παλαιού τύπου έργα και πρωτότυπα.

Service Locator σε Swift: καθολικό μητρώο υπηρεσιών

Service Locator σε Swift — υλοποίηση μέσω στατικών ιδιοτήτων και thread-safe λεξικού. Χρησιμοποιείται μητρώο με ObjectIdentifier(Protocol.self) ως κλειδί και εργοστάσια (()->Any) ως τιμές. Τεμπέλικη αρχικοποίηση (lazy var) — τυπική πρακτική: η υπηρεσία δημιουργείται στο πρώτο αίτημα. Η Swift απαιτεί ρητή μετατροπή τύπου στο resolve: guard let service = locator.resolve(ServiceProtocol.self) else { return }.

swift
final class ServiceLocator {
    static let shared = ServiceLocator()
    private var registry: [ObjectIdentifier: Any] = [:]
    private let lock = NSLock()

    func register<T>(_ type: T.Type, factory: @escaping () -> T) {
        lock.lock()
        registry[ObjectIdentifier(type)] = factory
        lock.unlock()
    }

    func resolve<T>(_ type: T.Type) -> T {
        lock.lock()
        defer { lock.unlock() }
        guard let factory = registry[ObjectIdentifier(type)] as? () -> T else {
            fatalError("Service \(type) not registered")
        }
        return factory()
    }

    func reset() {
        lock.lock()
        registry.removeAll()
        lock.unlock()
    }
}

// Καταχώρηση
ServiceLocator.shared.register(NetworkServiceProtocol.self) { NetworkService() }

// Χρήση
let network = ServiceLocator.shared.resolve(NetworkServiceProtocol.self)

Διαχείριση εμβέλειας — ο εντοπιστής μπορεί να αποθηκεύει εργοστάσια (transient — νέο αντικείμενο κάθε φορά) ή έτοιμα στιγμιότυπα (singleton). Για εργοστάσια καταχωρείται ένα closure που καλείται σε κάθε resolve. Για singleton — closure με σύλληψη του δημιουργημένου στιγμιότυπου. Προσθήκη εμβέλειας: .transient, .singleton, .weak (ασθενής αναφορά — το αντικείμενο ζει όσο κάποιος κρατά αναφορά). Η weak εμβέλεια είναι βολική για UIKit ViewController για αποφυγή διαρροών μνήμης σε pop/dismiss.

Service Locator σε Kotlin: τεμπέλικη καταχώρηση

Service Locator σε Kotlin — συμπαγής υλοποίηση μέσω object (Singleton) με inline reified συναρτήσεις για ασφάλεια τύπων. Η Kotlin επιτρέπει τη δημιουργία ενός συνοπτικού εντοπιστή: val service by locator<ServiceProtocol>() με εκπρόσωπο, κάνοντας τον κώδικα καθαρότερο. Τα Reified generics (<reified T>) αντικαθιστούν το ObjectIdentifier — ο τύπος λαμβάνεται από το generic. Οι εντοπιστές Kotlin χρησιμοποιούν συχνά ConcurrentHashMap για thread-safety χωρίς ρητές κλειδώσεις.

kotlin
object ServiceLocator {
    private val registry = ConcurrentHashMap<Class<*>, () -> Any>()

    inline fun <reified T: Any> register(noinline factory: () -> T) {
        registry[T::class.java] = factory
    }

    @Suppress("UNCHECKED_CAST")
    inline fun <reified T: Any> resolve(): T {
        val factory = registry[T::class.java]
            ?: throw IllegalStateException("Service ${T::class.simpleName} not registered")
        return factory() as T
    }

    fun clear() {
        registry.clear()
    }
}

// Καταχώρηση
ServiceLocator.register<ApiService> { RetrofitApiService() }

// Χρήση σε κλάση
class UserRepository {
    private val api: ApiService = ServiceLocator.resolve()
    private val db: Database = ServiceLocator.resolve()
}

// Εκπρόσωπος για τεμπέλικη επίλυση
class LocatorDelegate<reified T: Any> : Lazy<T> {
    override val value: T get() = ServiceLocator.<T>resolve()
    override fun isInitialized(): Boolean = true
}

inline fun <reified T: Any> locator(): Lazy<T> = LocatorDelegate()
// Χρήση: val api by locator()

Service Locator σε Android — συναντάται σε παλαιά έργα πριν από την εμφάνιση του Dagger. Τα Jetpack Hilt και Koin έχουν εκτοπίσει το Service Locator στην κοινότητα Android. Ωστόσο, ο εντοπιστής παραμένει σχετικός για δοκιμές μονάδας: μια απλή απομίμηση ServiceLocator με mock υπηρεσίες χωρίς Hilt. Πλεονέκτημα: δεν χρειάζεται αναμονή για μεταγλώττιση Dagger για δοκιμές. Μειονέκτημα: αν ξεχάσετε να αντικαταστήσετε τον εντοπιστή στη δοκιμή, οι δοκιμές χρησιμοποιούν υπηρεσίες παραγωγής.

Service Locator vs DI: σύγκριση και πότε να επιλέξετε τι

Διαφάνεια εξαρτήσεων — η κύρια διαφορά. Το DI δηλώνει εξαρτήσεις ρητά: init(service: ServiceProtocol) — οποιοδήποτε IDE δείχνει τις εξαρτήσεις της κλάσης. Το Service Locator τις κρύβει: dependencies = ServiceLocator.resolve() — κρυμμένο μέσα στη μέθοδο. Με DI μπορείτε να δείτε αμέσως όλες τις εξαρτήσεις της κλάσης, με Service Locator πρέπει να διαβάσετε ολόκληρο το σώμα της κλάσης. Αυτό καθιστά τον κώδικα με Service Locator λιγότερο προβλέψιμο: μια αλλαγή στο μητρώο μπορεί να σπάσει οποιαδήποτε κλάση χρησιμοποιεί τον εντοπιστή.

ΧαρακτηριστικόService LocatorDependency Injection
Ορατότητα εξαρτήσεωνΚρυμμένες στο σώμα μεθόδωνΡητές στον κατασκευαστή
ΈλεγχοςΡύθμιση καθολικού μητρώουMock στον κατασκευαστή
ΑρθρωτότηταΚαθολικό μητρώο — όχι αρθρωτόΕνότητες με διαφορετικά κοντέινερ
ΠολυπλοκότηταΑπλή υλοποίηση, 50-100 γραμμέςΑπαιτεί Dagger/Swinject
Time-to-marketΓρήγορη εκκίνησηΡύθμιση κοντέινερ

Πότε το Service Locator δικαιολογείται — πρωτότυπα και MVP (γρήγορη εκκίνηση χωρίς ρύθμιση). Παλαιού τύπου έργα όπου δεν μπορεί να προστεθεί πλαίσιο DI (πολύπλοκη μεταγλώττιση, περιορισμοί linter). Βιβλιοθήκες εργαλείων (καταγραφή, αναφορά σφαλμάτων) — είναι ήδη καθολικές. Για εφαρμογές παραγωγής με ομάδα 3 προγραμματιστών συνιστάται DI: οι ρητές εξαρτήσεις μειώνουν τον αριθμό σφαλμάτων κατά την αναδόμηση και διευκολύνουν την εισαγωγή νέων προγραμματιστών στο έργο.

Προβλήματα του Service Locator και εναλλακτικές

Κρυφές εξαρτήσεις — μια κλάση που χρησιμοποιεί ServiceLocator.resolve() μέσα σε μια μέθοδο δεν μπορεί να αναλυθεί στατικά. Το IDE δεν δείχνει εξαρτήσεις, ο μεταγλωττιστής δεν ελέγχει αν η υπηρεσία είναι καταχωρημένη. Το σφάλμα «Service not registered» εμφανίζεται μόνο κατά την εκτέλεση. Η αναδόμηση γίνεται επικίνδυνη: η αφαίρεση μιας υπηρεσίας από το μητρώο μπορεί να σπάσει οποιαδήποτε κλάση στην εφαρμογή. Το DI λύνει αυτό το πρόβλημα μέσω ελέγχων κατά τη μεταγλώττιση (Dagger) ή ρητών κατασκευαστών.

Πρόβλημα ελέγχου — κάθε δοκιμή πρέπει να ρυθμίσει το ServiceLocator.shared με όλες τις εξαρτήσεις. Μετά τη δοκιμή — επαναφορά (reset()) κατάστασης. Στην παράλληλη εκτέλεση δοκιμών, η καθολική κατάσταση του ServiceLocator.shared οδηγεί σε race condition: μια δοκιμή καταχωρεί ένα mock, μια άλλη — λαμβάνει το mock κάποιου άλλου. Λύση: εντοπιστές με εμβέλεια (ένας ανά δοκιμή) ή ThreadLocal. Το DI λύνει αυτό το πρόβλημα εκ βάθρων: κάθε δοκιμή δημιουργεί το δικό της στιγμιότυπο με mock εξαρτήσεις.

swift
// Πρόβλημα ελέγχου Service Locator
class LoginViewModelTests: XCTestCase {
    override func setUp() {
        super.setUp()
        // Ρύθμιση καθολικού μητρώου για δοκιμή
        ServiceLocator.shared.register(AuthServiceProtocol.self) { MockAuthService() }
    }

    override func tearDown() {
        ServiceLocator.shared.reset()
        super.tearDown()
    }

    func testLogin() {
        let viewModel = LoginViewModel() // χρησιμοποιεί ServiceLocator εσωτερικά
        // δοκιμή...
    }
}

Εναλλακτικές του Service Locator — DI (Dagger, Swinject), Factory Method, Ambient Context. Factory Method — απλό πρότυπο χωρίς καθολική κατάσταση: η κλάση εργοστασίου δημιουργεί υπηρεσίες και μεταβιβάζεται μέσω κατασκευαστή. Ambient Context — thread-safe εναλλακτική για διασταυρούμενες ανησυχίες (καταγραφή, εξουσιοδότηση). Η καλύτερη εναλλακτική — Constructor Injection με χειροκίνητο εργοστάσιο χωρίς πλαίσιο DI: η ρητή δημιουργία εξαρτήσεων στο εργοστάσιο με μεταβίβαση μέσω κατασκευαστή δίνει τη διαφάνεια DI χωρίς την πολυπλοκότητα ρύθμισης Dagger/Swinject.

Συχνές Ερωτήσεις

Είναι το Service Locator αντί-πρότυπο;

Πολλοί προγραμματιστές θεωρούν το Service Locator αντί-πρότυπο επειδή κρύβει εξαρτήσεις, δυσκολεύει τον έλεγχο και δημιουργεί κρυφές συνδέσεις μεταξύ κλάσεων. Ωστόσο, σε πρωτότυπα, μικρά έργα και για καθολικές υπηρεσίες (καταγραφή, αναλυτικά) το Service Locator μπορεί να δικαιολογηθεί. Η απόφαση εξαρτάται από το πλαίσιο: για εφαρμογή παραγωγής με ομάδα — DI, για έναν προγραμματιστή σε πρωτότυπο — Service Locator.

Σε τι διαφέρει το Service Locator από το κοντέινερ DI;

Το κοντέινερ DI (Dagger, Swinject) εγχέει εξαρτήσεις σε ένα αντικείμενο αυτόματα — το αντικείμενο δεν γνωρίζει την ύπαρξη του κοντέινερ. Service Locator — το αντικείμενο ζητά μόνο του εξαρτήσεις από το μητρώο. Το κοντέινερ DI τηρεί την αρχή IoC, το Service Locator — την παραβιάζει: το αντικείμενο διαχειρίζεται μόνο του τη λήψη των εξαρτήσεών του. Το κοντέινερ DI λειτουργεί πριν από τη δημιουργία του αντικειμένου (μέσω κατασκευαστή), το Service Locator — σε οποιοδήποτε σημείο του κώδικα.

Πότε να χρησιμοποιήσω Service Locator στο iOS;

Το Service Locator δικαιολογείται στο iOS για: καθολικές υπηρεσίες (Analytics, Logger, Crashlytics), πρωτότυπα όπου η ρύθμιση Swinject είναι υπερβολική, και για δοκιμές μονάδας μεγάλων παλαιού τύπου ενοτήτων. Για νέα έργα iOS συνιστάται Swinject ή χειροκίνητο DI μέσω κατασκευαστή. Το SwiftUI με Environment — επίσης μια μορφή DI που αποφεύγει το Service Locator.

Πώς να αποφύγετε το race condition στο Service Locator;

Χρησιμοποιήστε thread-safe συλλογή (NSLock σε Swift, ConcurrentHashMap σε Kotlin). Για δοκιμές — ThreadLocal ή εντοπιστή με εμβέλεια. Εναλλακτική: async-local storage — υπηρεσίες δεμένες σε coroutine/actor. Η καλύτερη λύση — αποφύγετε το Service Locator για παράλληλες δοκιμές και χρησιμοποιήστε DI με ρητή δημιουργία αντικειμένων για κάθε δοκιμή.

Είναι το Service Locator singleton;

Συνήθως το Service Locator υλοποιείται ως Singleton, αλλά αυτό δεν είναι υποχρεωτικό. Μπορείτε να δημιουργήσετε ένα στιγμιότυπο εντοπιστή για μια ενότητα (feature-scoped locator) και να το μεταβιβάσετε μέσω κατασκευαστή. Ο feature-scoped εντοπιστής λύνει το πρόβλημα της καθολικής κατάστασης, αλλά δεν λύνει το πρόβλημα των κρυφών εξαρτήσεων. Ένα τέτοιο πρότυπο ονομάζεται Ambient Context ή Scoped Locator.

Σύνοψη

  • Service Locator — κεντρικό μητρώο για λήψη υπηρεσιών μέσω στατικής πρόσβασης
  • Κρυφές εξαρτήσεις — κύριο μειονέκτημα: οι εξαρτήσεις δεν είναι ορατές στην υπογραφή της κλάσης
  • Έλεγχος — δυσκολότερος από το DI λόγω καθολικής κατάστασης και ανάγκης επαναφοράς
  • Swift/Kotlin — υλοποίηση μέσω thread-safe λεξικού και reified generics
  • Εναλλακτική — DI (Dagger Hilt, Swinject) — πρότυπο για έργα παραγωγής
  • Δικαιολογείται — σε πρωτότυπα, για καθολικές υπηρεσίες και παλαιού τύπου έργα

Θα αναπτύξουμε μια εφαρμογή για κινητά έτοιμη για χρήση

Η IT Sectr δημιουργεί εφαρμογές iOS και Android για νεοφυείς επιχειρήσεις και επιχειρήσεις από το 2017. Θα σας συμβουλεύσουμε και θα προτείνουμε την καλύτερη λύση.

Συζήτηση έργου

Διαβάστε επίσης