Service Locator là một mẫu kiến trúc cung cấp một registry trung tâm cho các dịch vụ. Mã client yêu cầu một dịch vụ thông qua một bộ định vị tĩnh, mà không tạo trực tiếp hoặc nhận qua hàm tạo. Service Locator thường được coi là một giải pháp thay thế cho Dependency Injection: triển khai đơn giản hơn, nhưng ẩn các phụ thuộc và làm phức tạp việc kiểm thử. Mẫu này được triển khai thông qua một lớp Singleton toàn cục với đăng ký và giải quyết dịch vụ. Chi tiết hơn — trong so sánh DI và Service Locator của Martin Fowler.
Những điểm chính
Service Locator là một mẫu tập trung hóa việc tạo và cung cấp dịch vụ. Nó dựa trên một lớp Singleton (Locator) chứa một Registry các dịch vụ: một từ điển nơi khóa là loại dịch vụ (hoặc định danh) và giá trị là triển khai cụ thể. Mã client gọi ServiceLocator.resolve(ServiceProtocol.self) và nhận một phiên bản sẵn sàng. Mẫu không quy định cách dịch vụ được tạo — factory, container DI hoặc new bên trong bộ định vị.
Cấu trúc của mẫu — Registry (từ điển loại [String: Any]), Locator (lớp tĩnh với register và resolve), Service (dịch vụ đang được đăng ký). Registry có thể lưu trữ các factory (closure/lambda để tạo đối tượng) hoặc các phiên bản sẵn sàng. Bộ định vị có thể là toàn cục (một cho mỗi ứng dụng) hoặc có phạm vi (theo tính năng/mô-đun). Giải quyết dịch vụ là tra cứu trong từ điển theo loại. Swift và Kotlin sử dụng loại làm khóa thông qua metatype: ObjectIdentifier(ServiceProtocol.self).
| Thành phần | Trách nhiệm | Swift/Kotlin |
|---|---|---|
| ServiceLocator | Truy cập toàn cục đến dịch vụ | class ServiceLocator |
| Registry | Lưu trữ factory/phiên bản | [ObjectIdentifier: Any] |
| Service | Triển khai cụ thể | NetworkService() |
Lịch sử của mẫu — Service Locator được mô tả trong sách Java Patterns (1998) và sau đó trong Core J2EE Patterns (2001). Bài viết năm 2004 của Martin Fowler so sánh Service Locator với DI, lưu ý rằng Service Locator là một "giải pháp thay thế đơn giản hơn, nhưng tệ hơn cho kiểm thử." Trong phát triển di động, Service Locator đã được sử dụng trong các dự án Android đầu tiên và ứng dụng iOS trước khi Dagger và Swinject xuất hiện. Hiện nay, mẫu này thường được tìm thấy trong các dự án kế thừa và nguyên mẫu.
Service Locator trong Swift — triển khai thông qua các thuộc tính tĩnh và từ điển an toàn luồng. Một registry với ObjectIdentifier(Protocol.self) làm khóa và factory (() -> Any) làm giá trị được sử dụng. Khởi tạo lười biếng (lazy var) là thực hành tiêu chuẩn: dịch vụ được tạo khi có yêu cầu đầu tiên. Swift yêu cầu ép kiểu tường minh trong quá trình resolve: guard let service = locator.resolve(ServiceProtocol.self) else { return }.
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()
}
}
// Đăng ký
ServiceLocator.shared.register(NetworkServiceProtocol.self) { NetworkService() }
// Sử dụng
let network = ServiceLocator.shared.resolve(NetworkServiceProtocol.self)
Quản lý phạm vi — bộ định vị có thể lưu trữ factory (transient — đối tượng mới mỗi lần) hoặc phiên bản sẵn sàng (singleton). Đối với factory, một closure được đăng ký và gọi mỗi lần resolve. Đối với singleton, một closure ghi lại phiên bản đã tạo. Thêm phạm vi: .transient, .singleton, .weak (tham chiếu yếu — đối tượng tồn tại khi ai đó giữ tham chiếu). Phạm vi weak thuận tiện cho UIViewController của UIKit để tránh rò rỉ khi pop/dismiss.
Service Locator trong Kotlin — một triển khai nhỏ gọn thông qua object (Singleton) với các hàm inline reified cho an toàn kiểu. Kotlin cho phép một bộ định vị ngắn gọn: val service by locator với một delegate, làm cho mã sạch hơn. Reified generics () thay thế ObjectIdentifier — kiểu được lấy từ generic. Các bộ định vị Kotlin thường sử dụng ConcurrentHashMap để an toàn luồng mà không cần khóa tường minh.
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()
}
}
// Đăng ký
ServiceLocator.register<ApiService> { RetrofitApiService() }
// Sử dụng trong lớp
class UserRepository {
private val api: ApiService = ServiceLocator.resolve()
private val db: Database = ServiceLocator.resolve()
}
// Ủy quyền cho giải quyết lười biếng
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()
// Sử dụng: val api by locator()
Service Locator trong Android — được tìm thấy trong các dự án cũ trước Dagger. Jetpack Hilt và Koin đã thay thế Service Locator trong cộng đồng Android. Tuy nhiên, bộ định vị vẫn phù hợp cho kiểm thử đơn vị: một ServiceLocator giả đơn giản với các dịch vụ mock không cần Hilt. Ưu điểm: không cần chờ biên dịch Dagger cho kiểm thử. Nhược điểm: nếu bạn quên ghi đè bộ định vị trong kiểm thử, các kiểm thử sẽ sử dụng dịch vụ sản xuất.
Tính tường minh của phụ thuộc — sự khác biệt chính. DI khai báo phụ thuộc một cách tường minh: init(service: ServiceProtocol) — bất kỳ IDE nào cũng hiển thị phụ thuộc của lớp. Service Locator ẩn chúng: dependencies = ServiceLocator.resolve() — ẩn bên trong phương thức. Với DI, bạn có thể thấy ngay tất cả phụ thuộc của lớp; với Service Locator, bạn cần đọc toàn bộ thân lớp. Điều này làm cho mã Service Locator kém dự đoán hơn: thay đổi registry có thể phá vỡ bất kỳ lớp nào sử dụng bộ định vị.
| Đặc điểm | Service Locator | Dependency Injection |
|---|---|---|
| Hiển thị phụ thuộc | Ẩn trong thân phương thức | Tường minh trong hàm tạo |
| Kiểm thử | Cấu hình registry toàn cục | Mock trong hàm tạo |
| Tính mô-đun | Registry toàn cục — không mô-đun | Mô-đun với container riêng |
| Độ phức tạp | Triển khai đơn giản, 50-100 dòng | Yêu cầu Dagger/Swinject |
| Thời gian ra thị trường | Khởi đầu nhanh | Cần cấu hình container |
Khi nào Service Locator được biện minh — nguyên mẫu và MVP (khởi đầu nhanh không cần cấu hình). Dự án kế thừa nơi không thể thêm framework DI (xây dựng phức tạp, hạn chế của linter). Thư viện công cụ (ghi log, báo cáo sự cố) — chúng đã là toàn cục. Đối với ứng dụng sản xuất với đội ngũ 3+ nhà phát triển, DI được khuyến nghị: phụ thuộc tường minh giảm số lỗi khi tái cấu trúc và đơn giản hóa việc giới thiệu nhà phát triển mới.
Phụ thuộc bị ẩn — một lớp sử dụng ServiceLocator.resolve() bên trong phương thức không thể được phân tích tĩnh. IDE không hiển thị phụ thuộc, trình biên dịch không kiểm tra dịch vụ đã được đăng ký chưa. Lỗi "Service not registered" chỉ xảy ra khi chạy. Tái cấu trúc trở nên nguy hiểm: xóa một dịch vụ khỏi registry có thể phá vỡ bất kỳ lớp nào trong ứng dụng. DI giải quyết vấn đề này thông qua kiểm tra lúc biên dịch (Dagger) hoặc hàm tạo tường minh.
Vấn đề kiểm thử — mỗi bài kiểm thử phải cấu hình ServiceLocator.shared với tất cả phụ thuộc. Sau kiểm thử — đặt lại trạng thái. Trong quá trình thực thi kiểm thử song song, trạng thái toàn cục của ServiceLocator.shared dẫn đến điều kiện tranh đua: một kiểm thử đăng ký mock, kiểm thử khác nhận mock của người khác. Giải pháp: bộ định vị có phạm vi (một cho mỗi kiểm thử) hoặc ThreadLocal. DI giải quyết vấn đề này ngay từ đầu: mỗi kiểm thử tạo phiên bản riêng với phụ thuộc mock.
// Vấn đề kiểm thử Service Locator
class LoginViewModelTests: XCTestCase {
override func setUp() {
super.setUp()
// Thiết lập registry toàn cục cho kiểm thử
ServiceLocator.shared.register(AuthServiceProtocol.self) { MockAuthService() }
}
override func tearDown() {
ServiceLocator.shared.reset()
super.tearDown()
}
func testLogin() {
let viewModel = LoginViewModel() // sử dụng ServiceLocator nội bộ
// kiểm thử...
}
}
Giải pháp thay thế cho Service Locator — DI (Dagger, Swinject), Factory Method, Ambient Context. Factory Method — một mẫu đơn giản không có trạng thái toàn cục: một lớp factory tạo dịch vụ và được truyền qua hàm tạo. Ambient Context — một giải pháp thay thế an toàn luồng cho các mối quan tâm xuyên suốt (ghi log, ủy quyền). Giải pháp thay thế tốt nhất là Constructor Injection với factory thủ công không cần framework DI: tạo phụ thuộc tường minh trong factory với truyền qua hàm tạo mang lại sự rõ ràng của DI mà không cần độ phức tạp của việc cấu hình Dagger/Swinject.
Câu hỏi thường gặp
Nhiều nhà phát triển coi Service Locator là phản mẫu vì nó ẩn phụ thuộc, làm phức tạp kiểm thử và tạo sự kết nối ẩn giữa các lớp. Tuy nhiên, trong nguyên mẫu, dự án nhỏ và cho các dịch vụ toàn cục (ghi log, phân tích), Service Locator có thể được biện minh. Quyết định phụ thuộc vào ngữ cảnh: cho ứng dụng sản xuất với đội ngũ — DI, cho nhà phát triển đơn lẻ trên nguyên mẫu — Service Locator.
Container DI (Dagger, Swinject) tự động tiêm phụ thuộc vào đối tượng — đối tượng không biết về sự tồn tại của container. Service Locator — đối tượng tự yêu cầu phụ thuộc từ registry. Container DI tuân theo nguyên tắc IoC; Service Locator vi phạm nó: đối tượng tự quản lý việc lấy phụ thuộc của chính mình. Container DI hoạt động trước khi tạo đối tượng (qua hàm tạo), Service Locator — ở bất kỳ đâu trong mã.
Service Locator được biện minh trong iOS cho: dịch vụ toàn cục (Analytics, Logger, Crashlytics), nguyên mẫu nơi cấu hình Swinject là quá mức cần thiết, và cho kiểm thử đơn vị của các mô-đun kế thừa lớn. Cho dự án iOS mới, Swinject hoặc DI thủ công qua hàm tạo được khuyến nghị. SwiftUI với @Environment — cũng là một dạng của DI, tránh Service Locator.
Sử dụng bộ sưu tập an toàn luồng (NSLock trong Swift, ConcurrentHashMap trong Kotlin). Cho kiểm thử — ThreadLocal hoặc bộ định vị có phạm vi. Giải pháp thay thế: lưu trữ async-local — dịch vụ gắn với coroutine/actor. Giải pháp tốt nhất là tránh Service Locator cho kiểm thử song song và sử dụng DI với tạo đối tượng tường minh cho mỗi kiểm thử.
Service Locator thường được triển khai như Singleton, nhưng điều này không bắt buộc. Bạn có thể tạo một phiên bản bộ định vị cho một mô-đun (bộ định vị theo phạm vi tính năng) và truyền nó qua hàm tạo. Bộ định vị theo phạm vi tính năng giải quyết vấn đề trạng thái toàn cục nhưng không giải quyết vấn đề phụ thuộc bị ẩn. Mẫu này được gọi là Ambient Context hoặc Scoped Locator.
Tổng kết
Chúng tôi sẽ phát triển ứng dụng di động chìa khóa trao tay
IT Sectr tạo các ứng dụng iOS và Android cho các công ty khởi nghiệp và doanh nghiệp từ năm 2017. Chúng tôi sẽ tư vấn và đề xuất giải pháp tốt nhất cho bạn.
Đọc thêm