Service Locator — แก่นแท้ของรูปแบบ, ทะเบียนกลางและ DI

ผู้แต่ง: IT Sectr เผยแพร่เมื่อ: 2026-02-18 เวลาอ่าน: 9 นาที

Service Locator เป็นรูปแบบสถาปัตยกรรมที่ให้ทะเบียนบริการกลาง โค้ดไคลเอนต์ขอบริการผ่านตัวระบุตำแหน่งแบบสแตติก โดยไม่ต้องสร้างโดยตรงหรือรับผ่านคอนสตรัคเตอร์ Service Locator มักถูกพิจารณาว่าเป็นทางเลือกของ Dependency Injection: ใช้งานได้ง่ายกว่า แต่ซ่อนการพึ่งพาและทำให้การทดสอบซับซ้อน รูปแบบนี้ถูกใช้งานผ่านคลาส Singleton ทั่วโลกที่มีการลงทะเบียนและแก้ไขบริการ รายละเอียดเพิ่มเติม — ใน การเปรียบเทียบ DI และ Service Locator โดย Martin Fowler

ประเด็นสำคัญ

  • Service Locator — ทะเบียนกลางสำหรับรับบริการ
  • ทางเลือกของ DI — ใช้งานได้ง่ายกว่า แต่ซ่อนการพึ่งพาจากไคลเอนต์
  • รูปแบบต้าน — นักพัฒนาหลายคนถือว่า Service Locator เป็นรูปแบบต้านเนื่องจากการพึ่งพาที่ซ่อนอยู่
  • การเข้าถึงทั่วโลก — ตัวระบุตำแหน่งสามารถเข้าถึงแบบสแตติกได้จากทุกที่ โดยไม่ต้องส่งผ่านคอนสตรัคเตอร์
  • การทดสอบ — ซับซ้อนกว่า DI: ต้องกำหนดค่าตัวระบุตำแหน่งทั่วโลกสำหรับแต่ละการทดสอบ

Service Locator คืออะไร: แก่นแท้และโครงสร้างของรูปแบบ

Service Locator เป็นรูปแบบที่รวมศูนย์การสร้างและการให้บริการ มันขึ้นอยู่กับคลาส Singleton (Locator) ที่มีทะเบียนบริการ: พจนานุกรมที่คีย์เป็นประเภทบริการ (หรือตัวระบุ) และค่าเป็นการใช้งานที่เป็นรูปธรรม โค้ดไคลเอนต์เรียก ServiceLocator.resolve(ServiceProtocol.self) และได้รับอินสแตนซ์ที่พร้อมใช้งาน รูปแบบไม่ได้กำหนดว่าบริการถูกสร้างขึ้นอย่างไร — โรงงาน คอนเทนเนอร์ DI หรือ new ภายในตัวระบุตำแหน่ง

โครงสร้างของรูปแบบ — ทะเบียน (พจนานุกรมประเภท [String: Any]), ตัวระบุตำแหน่ง (คลาสสแตติกที่มี register และ resolve), บริการ (บริการที่กำลังลงทะเบียน) ทะเบียนสามารถเก็บโรงงาน (closure/lambda สำหรับสร้างวัตถุ) หรืออินสแตนซ์ที่พร้อมใช้งาน ตัวระบุตำแหน่งสามารถเป็นทั่วโลก (หนึ่งต่อแอปพลิเคชัน) หรือแบบมีขอบเขต (ต่อคุณลักษณะ/โมดูล) การแก้ไขบริการคือการค้นหาในพจนานุกรมตามประเภท Swift และ Kotlin ใช้ประเภทเป็นคีย์ผ่านเมตาไทป์: ObjectIdentifier(ServiceProtocol.self)

ส่วนประกอบความรับผิดชอบSwift/Kotlin
ServiceLocatorการเข้าถึงบริการทั่วโลกclass ServiceLocator
Registryที่เก็บโรงงาน/อินสแตนซ์[ObjectIdentifier: Any]
Serviceการใช้งานที่เป็นรูปธรรมNetworkService()

ประวัติของรูปแบบ — Service Locator ถูกอธิบายในหนังสือ Java Patterns (1998) และต่อมาใน Core J2EE Patterns (2001) บทความปี 2004 ของ Martin Fowler เปรียบเทียบ Service Locator กับ DI โดยระบุว่า Service Locator เป็น "ทางเลือกที่ง่ายกว่า แต่แย่กว่าสำหรับการทดสอบ" ในการพัฒนามือถือ Service Locator ถูกใช้ในโปรเจกต์ Android ยุคแรกและแอป iOS ก่อนการมาถึงของ Dagger และ Swinject ปัจจุบันรูปแบบนี้พบได้บ่อยในโปรเจกต์เดิมและต้นแบบ

Service Locator ใน Swift: ทะเบียนบริการทั่วโลก

Service Locator ใน Swift — การใช้งานผ่านคุณสมบัติสแตติกและพจนานุกรมที่ปลอดภัยต่อเธรด ใช้ทะเบียนที่มี 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 สะดวกสำหรับ UIViewController ของ UIKit เพื่อหลีกเลี่ยงการรั่วไหลเมื่อ pop/dismiss

Service Locator ใน Kotlin: การลงทะเบียนแบบขี้เกียจ

Service Locator ใน Kotlin — การใช้งานที่กะทัดรัดผ่าน object (Singleton) พร้อมฟังก์ชัน inline reified เพื่อความปลอดภัยของประเภท Kotlin อนุญาตให้สร้างตัวระบุตำแหน่งที่กระชับ: val service by locator() พร้อมตัวแทน ทำให้โค้ดสะอาดขึ้น Reified generics () แทนที่ ObjectIdentifier — ประเภทได้มาจากเจเนอริก ตัวระบุตำแหน่ง Kotlin มักใช้ ConcurrentHashMap เพื่อความปลอดภัยต่อเธรดโดยไม่ต้องล็อกอย่างชัดเจน

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 stub ง่าย ๆ พร้อมบริการ 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
เวลาในการออกสู่ตลาดเริ่มต้นรวดเร็วต้องตั้งค่าคอนเทนเนอร์

เมื่อใด Service Locator สมเหตุสมผล — ต้นแบบและ MVP (เริ่มต้นรวดเร็วโดยไม่ต้องกำหนดค่า) โปรเจกต์เดิมที่ไม่สามารถเพิ่มเฟรมเวิร์ก DI ได้ (การ build ที่ซับซ้อน ข้อจำกัดของ linter) ไลบรารีเครื่องมือ (การบันทึก, การรายงานข้อขัดข้อง) — มันเป็นทั่วโลกอยู่แล้ว สำหรับแอปพลิเคชันจริงที่มีทีม 3+ นักพัฒนา DI เป็นที่แนะนำ: การพึ่งพาที่ชัดเจนลดจำนวนข้อผิดพลาดระหว่างการปรับโครงสร้างและทำให้การเริ่มต้นนักพัฒนาใหม่ง่ายขึ้น

ปัญหาของ Service Locator และทางเลือก

การพึ่งพาที่ซ่อนอยู่ — คลาสที่ใช้ ServiceLocator.resolve() ภายในเมธอดไม่สามารถวิเคราะห์แบบสแตติกได้ IDE ไม่แสดงการพึ่งพา คอมไพเลอร์ไม่ตรวจสอบว่าบริการถูกลงทะเบียนหรือไม่ ข้อผิดพลาด "Service not registered" เกิดขึ้นเฉพาะตอนรันไทม์ การปรับโครงสร้างกลายเป็นอันตราย: การลบบริการออกจากทะเบียนสามารถทำลายคลาสใด ๆ ในแอปพลิเคชัน DI แก้ปัญหานี้ผ่านการตรวจสอบเวลาคอมไพล์ (Dagger) หรือคอนสตรัคเตอร์ที่ชัดเจน

ปัญหาการทดสอบ — แต่ละการทดสอบต้องกำหนดค่า ServiceLocator.shared ด้วยการพึ่งพาทั้งหมด หลังการทดสอบ — รีเซ็ตสถานะ ระหว่างการดำเนินการทดสอบแบบขนาน สถานะทั่วโลกของ ServiceLocator.shared นำไปสู่สภาวะการแข่งขัน: การทดสอบหนึ่งลงทะเบียน 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 — ทางเลือกที่ปลอดภัยต่อเธรดสำหรับความกังวลข้ามส่วน (การบันทึก, การอนุญาต) ทางเลือกที่ดีที่สุดคือ 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

จะหลีกเลี่ยงสภาวะการแข่งขันใน Service Locator ได้อย่างไร?

ใช้คอลเล็กชันที่ปลอดภัยต่อเธรด (NSLock ใน Swift, ConcurrentHashMap ใน Kotlin) สำหรับการทดสอบ — ThreadLocal หรือตัวระบุตำแหน่งแบบมีขอบเขต ทางเลือก: พื้นที่เก็บข้อมูล async-local — บริการผูกกับ coroutine/actor ทางออกที่ดีที่สุดคือหลีกเลี่ยง Service Locator สำหรับการทดสอบแบบขนานและใช้ DI พร้อมการสร้างวัตถุอย่างชัดเจนสำหรับแต่ละการทดสอบ

Service Locator เป็นซิงเกิลตันหรือไม่?

Service Locator มักถูกใช้งานเป็น Singleton แต่ไม่จำเป็น คุณสามารถสร้างอินสแตนซ์ตัวระบุตำแหน่งสำหรับโมดูล (feature-scoped locator) และส่งผ่านคอนสตรัคเตอร์ ตัวระบุตำแหน่งแบบมีขอบเขตคุณลักษณะแก้ปัญหาสถานะทั่วโลกแต่ไม่แก้ปัญหาการพึ่งพาที่ซ่อนอยู่ รูปแบบนี้เรียกว่า Ambient Context หรือ Scoped Locator

สรุป

  • Service Locator — ทะเบียนกลางสำหรับรับบริการผ่านการเข้าถึงแบบสแตติก
  • การพึ่งพาที่ซ่อนอยู่ — ข้อเสียหลัก: การพึ่งพาไม่ปรากฏในลายเซ็นคลาส
  • การทดสอบ — ซับซ้อนกว่า DI เนื่องจากสถานะทั่วโลกและความจำเป็นในการรีเซ็ต
  • Swift/Kotlin — การใช้งานผ่านพจนานุกรมที่ปลอดภัยต่อเธรดและ reified generics
  • ทางเลือก — DI (Dagger Hilt, Swinject) — มาตรฐานสำหรับโปรเจกต์จริง
  • สมเหตุสมผล — ในต้นแบบ สำหรับบริการทั่วโลก และโปรเจกต์เดิม

เราจะพัฒนาแอปพลิเคชันบนมือถือแบบครบวงจร

IT Sectr สร้างแอปพลิเคชัน iOS และ Android สำหรับสตาร์ทอัพและธุรกิจตั้งแต่ปี 2017 เราจะให้คำแนะนำและเสนอวิธีแก้ปัญหาที่ดีที่สุดแก่คุณ

ปรึกษาโครงการ

อ่านเพิ่มเติม