Service Locator
registry กลางที่ object ร้องขอ dependency ตอน runtime — มักเป็น antipattern
Service Locator แก้ปัญหาที่ดูเหมือนเรียบง่าย: object หนึ่งต้องการใช้ service อื่น (เช่น repository, logger, gateway) แล้วจะ “หา” service นั้นมาได้อย่างไรโดยไม่ต้อง new ขึ้นเองตรง ๆ และไม่ต้องผูกกับ implementation class เจาะจง แนวคิดของ pattern นี้คือสร้าง registry กลาง ที่รู้จักและเก็บ instance ของ service ทุกตัวใน application ไว้ ใครก็ตามที่ต้องการ dependency ก็แค่ถาม (pull) จาก registry นี้ ณ จุดที่ต้องใช้งาน แทนที่จะรับมันมาแบบชัดเจนผ่าน constructor
Martin Fowler บันทึกแนวคิดนี้ไว้ตั้งแต่ปี 2004 ควบคู่กับ Dependency Injection โดยชี้ว่าทั้งสองแบบต่างก็เป็นการ “แยก concern ของการประกอบ (assembly) ออกจากการใช้งาน (use)” — ต่างกันตรงที่ DI เป็นการ push dependency เข้าไปให้ตั้งแต่ตอนสร้าง object (inversion of control ที่แท้จริง) ส่วน Service Locator เป็นการ pull dependency ออกมาจาก registry ตอนที่ code ข้างในทำงานไปแล้ว
ปัญหาคือการ pull แบบนี้ทำให้ dependency กลายเป็นสิ่งที่มองไม่เห็นจาก API — constructor ที่ไม่มี parameter ดูเหมือนไม่ต้องการอะไรเลย ทั้งที่จริงพึ่งพา service หลายตัวซ่อนอยู่ใน method ละเมิด Explicit Dependencies Principle โดยตรง แม้เคยได้รับความนิยมมากในยุค Java EE และ .NET ยุคก่อนที่ DI container จะเป็นมาตรฐาน แต่ประสบการณ์การใช้งานจริงในวงกว้างแสดงว่ามันสร้างปัญหาการทดสอบและการบำรุงรักษามากกว่าที่แก้ จึงถูกนักเขียนหลายคน (เช่น Mark Seemann) จัดเป็น antipattern — กรณีทั่วไปควรใช้ dependency injection แทน
โครงสร้าง
หัวข้อที่มีชื่อว่า “โครงสร้าง”classDiagram
class Client {
+CompleteOrder()
}
class ServiceLocator {
-services Map
+GetService(type) object
+Register(type, instance)
}
class IOrderRepository {
+Save(order)
}
class SqlOrderRepository {
+Save(order)
}
class InMemoryOrderRepository {
+Save(order)
}
Client --> ServiceLocator
ServiceLocator o-- IOrderRepository
IOrderRepository <|.. SqlOrderRepository
IOrderRepository <|.. InMemoryOrderRepository
Client ไม่รู้จัก SqlOrderRepository โดยตรง — มันรู้จักแค่ ServiceLocator และเรียก GetService() เพื่อขอ instance ของ IOrderRepository ณ จุดที่ต้องใช้ ServiceLocator เก็บ map ระหว่าง type (หรือ key) กับ instance ที่ถูก register ไว้ล่วงหน้า มันจึงทำหน้าที่เหมือน DI container ตัวหนึ่ง แต่ต่างกันตรงจุดที่ client code ต้อง “รู้จักและเรียกหา” registry เอง แทนที่ registry จะฉีด instance ให้ตั้งแต่แรก
การทำงาน
หัวข้อที่มีชื่อว่า “การทำงาน”การทำงานแบ่งเป็นสองช่วงชัดเจน คือช่วง ลงทะเบียน (registration) ตอน startup และช่วง ค้นหา (resolution) ตอน runtime ทุกครั้งที่ code ต้องการ dependency
sequenceDiagram
participant App as Application Startup
participant SL as ServiceLocator
participant Client as CheckoutService
participant Repo as SqlOrderRepository
App->>SL: Register IOrderRepository กับ SqlOrderRepository
Client->>SL: GetService of IOrderRepository
SL->>SL: ค้นหาใน registry ภายใน
SL-->>Client: คืน instance ของ SqlOrderRepository
Client->>Repo: Save order
- ตอน startup application register ทุก implementation ที่ต้องการไว้ใน locator ล่วงหน้า (คล้ายขั้นตอน composition root ของ DI container)
- ตอนทำงานจริง เมื่อ
CheckoutServiceต้องการIOrderRepositoryมันเรียกServiceLocator.GetService<IOrderRepository>()โดยตรงจากภายใน method ไม่ใช่รับผ่าน constructor - locator ค้นหาใน map ภายใน ถ้าเจอก็คืน instance กลับ ถ้าไม่เจอ — เพราะลืม register หรือพิมพ์ type ผิด — จะได้ exception ตอน runtime ไม่ใช่ตอน compile
CheckoutServiceใช้ instance ที่ได้มาทำงานต่อ โดยที่ signature ของ constructor ไม่เคยบอกเลยว่า class นี้ต้องพึ่งพาIOrderRepository
จุดสำคัญคือ locator เอง “รู้จักวิธีสร้าง/หา service ทุกตัว” เหมือน DI container แต่ การควบคุมยังอยู่ที่ client — client ต้องเป็นฝ่ายเอื้อมมือไปหยิบ ไม่ใช่ container เป็นฝ่ายยื่นให้ จึงทำให้หลายคนมองว่านี่คือ Inversion of Control ที่ไม่สมบูรณ์ — ยังคง Control ไว้ที่ client เหมือนเดิม เพียงแต่เปลี่ยนจาก new SqlOrderRepository() เป็น ServiceLocator.GetService<IOrderRepository>()
ตัวอย่าง code
หัวข้อที่มีชื่อว่า “ตัวอย่าง code”// version Service Locator — แสดงให้เห็นปัญหา ไม่แนะนำให้ใช้ใน code ใหม่public static class ServiceLocator{ private static readonly Dictionary<Type, object> _services = new();
public static void Register<TService>(TService instance) => _services[typeof(TService)] = instance!;
public static TService GetService<TService>() { if (_services.TryGetValue(typeof(TService), out var service)) return (TService)service;
// ปัญหาหลัก: exception นี้เกิดตอน runtime ไม่ใช่ตอน compile throw new InvalidOperationException( $"ไม่พบ service ชนิด {typeof(TService).Name} ใน registry — ลืม Register หรือเปล่า"); }}
public interface IOrderRepository{ void Save(Order order);}
public class SqlOrderRepository : IOrderRepository{ public void Save(Order order) { /* บันทึกลงฐานข้อมูลจริง */ }}
// ✗ dependency ซ่อน — constructor ว่างดูเหมือนไม่ต้องการอะไรเลยpublic class CheckoutService{ public void CompleteOrder(Order order) { // ดึง dependency จาก registry กลาง ณ จุดใช้งาน — มองไม่เห็นจาก signature ภายนอก var repo = ServiceLocator.GetService<IOrderRepository>(); repo.Save(order); }}
// ตอน startup ต้อง register ทุก service ไว้ก่อน มิเช่นนั้น CheckoutService จะพังตอน runtimeServiceLocator.Register<IOrderRepository>(new SqlOrderRepository());var checkout = new CheckoutService();checkout.CompleteOrder(new Order());
// ✓ ทางเลือกที่แนะนำ — Dependency Injection ผ่าน constructorpublic class CheckoutServiceWithDi{ private readonly IOrderRepository _repo;
// ประกาศให้เห็นชัดว่าต้องการอะไร — compiler บังคับให้ผู้เรียกส่งมาให้ครบ public CheckoutServiceWithDi(IOrderRepository repo) => _repo = repo;
public void CompleteOrder(Order order) => _repo.Save(order);}
// ทดสอบง่ายกว่ามาก — ไม่ต้อง setup/teardown registry กลางที่ใช้ร่วมกันทั้ง appvar fakeRepo = new Mock<IOrderRepository>();var sut = new CheckoutServiceWithDi(fakeRepo.Object);sut.CompleteOrder(new Order());fakeRepo.Verify(r => r.Save(It.IsAny<Order>()), Times.Once);สังเกตว่า CheckoutServiceWithDi ทดสอบได้ตรงไปตรงมา — สร้าง instance พร้อม mock แล้วเรียกใช้ ไม่ต้อง register อะไรกับ global state ก่อน ในขณะที่ CheckoutService version Service Locator ทุก test ต้อง register IOrderRepository ไว้ใน ServiceLocator static ก่อนเสมอ และต้องระวังไม่ให้ test แต่ละตัวรบกวน state ที่ใช้ร่วมกัน
เมื่อไหร่ควรใช้
หัวข้อที่มีชื่อว่า “เมื่อไหร่ควรใช้”- โครงสร้างแบบ plugin/add-in ที่ implementation จริงยังไม่รู้จักตอน compile time และต้องค้นหา/โหลดแบบ dynamic ตอน runtime
- code legacy ที่ framework เป็นผู้สร้าง object ให้เอง (เช่น ASP.NET WebForms Page, บาง handler ของ message queue) จนไม่มีทางควบคุม constructor ได้โดยตรง — Service Locator เป็นสะพานชั่วคราวไปสู่ dependency ที่ต้องการ
- เครื่องมือ/script ขนาดเล็กมากที่การตั้ง DI container เต็มรูปแบบสิ้นเปลืองเกินความจำเป็นของงาน
- ต้องอินทิเกรตกับ framework ภายนอกที่บังคับให้เข้าถึง service แบบ static/global อยู่แล้ว (เช่น บาง game engine หรือ SDK ของบุคคลที่สาม)
เมื่อไหร่ไม่ควรใช้
หัวข้อที่มีชื่อว่า “เมื่อไหร่ไม่ควรใช้”- project ใหม่ทุกกรณีที่มี DI container ให้ใช้อยู่แล้ว (เช่น
Microsoft.Extensions.DependencyInjectionของ ASP.NET Core) — ใช้ constructor injection แทนได้เสมอ - ในชั้น domain logic หรือ business logic ที่ควรทดสอบได้ง่ายและไม่ควรรู้จัก infrastructure concern ใด ๆ เลย รวมถึง registry กลาง
- เมื่อทีมให้ความสำคัญกับ unit test ที่รวดเร็วและแยกจากกัน (isolated) — global state ที่ share ข้าม test จะสร้างปัญหา flaky test
- เมื่อทีมต้องการให้ compiler ช่วยจับ dependency ที่ขาดหายตั้งแต่ตอน build ไม่ใช่รอเจอ exception ตอน production
ข้อดีและข้อเสีย
หัวข้อที่มีชื่อว่า “ข้อดีและข้อเสีย”| ด้าน | รายละเอียด |
|---|---|
| ข้อดี | สลับ implementation ตอน runtime ได้โดยไม่ต้อง compile ใหม่ |
| ข้อดี | มีประโยชน์เป็นสะพานใน code legacy ที่ยังไม่มี composition root หรือ DI framework |
| ข้อดี | รองรับ registry หลายชุดแยกตาม context หรือ scope (เช่น test locator แยกจาก production locator) |
| ข้อเสีย | ซ่อน dependency ไว้ใน method — ละเมิด Explicit Dependencies Principle |
| ข้อเสีย | error ที่ควรจับได้ตอน compile กลายเป็น exception ตอน runtime |
| ข้อเสีย | ทุก class ต้องรู้จักและผูกกับ locator เอง — เพิ่ม coupling กับ infrastructure ทั่วทั้ง app |
| ข้อเสีย | ทดสอบยากขึ้น เพราะต้อง setup/teardown global registry ระหว่าง test แต่ละตัว |
| ข้อเสีย | เพิ่มความเสี่ยงตอน refactor เพราะ compiler ช่วยบอกไม่ได้ว่าใครพึ่งพา service ตัวไหนบ้าง |
ที่เกี่ยวข้อง
หัวข้อที่มีชื่อว่า “ที่เกี่ยวข้อง”- Dependency Injection
- Explicit Dependencies Principle
- Inversion of Control
- Dependency Inversion Principle
- Singleton
- Abstract Factory
แหล่งอ้างอิง
หัวข้อที่มีชื่อว่า “แหล่งอ้างอิง”- ที่มา · deviq.com/design-patterns/service-locator-pattern
- Service locator pattern — Wikipedia
- Inversion of Control Containers and the Dependency Injection pattern — Martin Fowler
- Service Locator is an Anti-Pattern — Mark Seemann (ploeh blog)
- Design Patterns Explained – Service Locator Pattern — Stackify