Singleton
รับประกันว่ามี instance เดียว — แต่มักเป็น antipattern
Singleton เป็น creational pattern ที่แก้ปัญหาสองอย่างพร้อมกัน หนึ่งคือ ควบคุมการสร้าง instance ให้ type หนึ่งมีอยู่ในระบบได้เพียงตัวเดียวเท่านั้น สองคือ ให้จุดเข้าถึงแบบ global (global access point) ที่ทุกส่วนของโปรแกรมเรียกใช้ instance เดียวกันนั้นได้จากทุกที่ โดยไม่ต้องส่งต่อ reference ผ่าน constructor หรือ parameter
แนวคิดดั้งเดิมของ Gang of Four ทำสิ่งนี้ด้วยการซ่อน constructor ไว้เป็น private แล้วเปิด static method หรือ static property (เช่น Instance) ให้เป็นทางเดียวที่จะเข้าถึง object นั้น เมื่อเรียกครั้งแรก class จะสร้าง instance ขึ้นมาเก็บไว้ใน static field แล้วคืนตัวเดิมซ้ำ ๆ ในทุกการเรียกครั้งถัดไป
ปัญหาคือวิธีนี้ผูก “การบังคับให้มีตัวเดียว” เข้ากับตัว type เอง ทำให้ class นั้นมีความรับผิดชอบสองอย่างซ้อนกัน — ทั้งทำงานตามหน้าที่ปกติ และคุม lifecycle ของตัวเอง — ซึ่งขัดกับ Single Responsibility Principle โดยตรง และการที่ code ทั่วทั้งระบบอ้างถึง Singleton.Instance แบบ static ทำให้เกิด tight coupling ที่เรียกว่า static cling — ยาก mock ยากทดสอบ และซ่อน dependency ที่แท้จริงของ class ไว้ไม่ให้เห็นจาก signature ภายนอก แนวทางที่ community สมัยใหม่แนะนำคือยึด Explicit Dependencies Principle แล้วใช้ dependency injection ฉีด instance เข้าไป พร้อมตั้งค่า IoC container ให้จัดการ lifetime แบบ singleton แทนที่จะให้ type บังคับตัวเอง
โครงสร้าง
หัวข้อที่มีชื่อว่า “โครงสร้าง”classDiagram
class Singleton {
-Singleton instance$
-Singleton()
+GetInstance() Singleton$
+DoSomething()
}
class ClientA {
+Run()
}
class ClientB {
+Run()
}
ClientA --> Singleton
ClientB --> Singleton
จุดสำคัญของโครงสร้างคือ constructor เป็น private เพื่อกันไม่ให้ใครสร้าง instance เพิ่ม ตัวแปร instance เป็น static field เก็บ instance เดียวนั้นไว้ในระดับ class ไม่ใช่ object และ GetInstance() เป็น static method ที่ทำหน้าที่ทั้งสร้าง (ครั้งแรก) และคืน instance เดิม (ครั้งถัดไป) จะสังเกตได้ว่า ClientA และ ClientB ต่างก็ผูก (-->) กับ Singleton โดยตรง — นี่คือจุดที่ทำให้เกิด coupling แบบ static cling เพราะ client ไม่ได้รับ dependency ผ่าน constructor หรือ interface แต่ไป “เอื้อม” หาตัว concrete class เองทุกครั้ง
การทำงาน
หัวข้อที่มีชื่อว่า “การทำงาน”เมื่อ client เรียก GetInstance() เป็นครั้งแรก class จะตรวจสอบว่า static field instance ยังเป็น null อยู่หรือไม่ ถ้าใช่ก็สร้าง instance ใหม่แล้วเก็บไว้ จากนั้นคืนค่ากลับไป ในการเรียกครั้งถัดไปทุกครั้ง เงื่อนไขจะพบว่า instance มีค่าแล้ว จึงคืน reference ตัวเดิมทันทีโดยไม่สร้างใหม่ — นี่คือ lazy initialization แบบพื้นฐาน
sequenceDiagram
participant ClientA
participant ClientB
participant Singleton
ClientA->>Singleton: GetInstance
Singleton->>Singleton: instance is null, create new
Singleton-->>ClientA: return instance
ClientB->>Singleton: GetInstance
Singleton-->>ClientB: return same instance
ในระบบ single-thread โฟลว์นี้ตรงไปตรงมา แต่ในระบบ multi-thread ถ้า2 thread เรียก GetInstance() พร้อมกันในจังหวะที่ instance ยังเป็น null ทั้งคู่ ทั้ง2 thread อาจผ่านเงื่อนไขตรวจสอบไปพร้อมกันและสร้าง instance คนละตัว ทำให้ “รับประกันว่ามีตัวเดียว” ที่เป็นเจตนาหลักของ pattern พังลง จึงต้องมีกลไก synchronization เพิ่ม เช่น lock, double-checked locking, static initialization ของ CLR หรือใช้ Lazy<T> ซึ่งจัดการ thread-safety ให้อัตโนมัติ
ตัวอย่าง code
หัวข้อที่มีชื่อว่า “ตัวอย่าง code”// รูปแบบคลาสสิกตาม GoF — thread-safe ด้วย Lazy<T> แต่ยังเป็น static clingpublic sealed class ConfigurationManager{ // Lazy<T> รับประกัน thread-safety และ lazy initialization ให้อัตโนมัติ private static readonly Lazy<ConfigurationManager> _lazyInstance = new(() => new ConfigurationManager());
private readonly Dictionary<string, string> _settings;
// constructor เป็น private เพื่อกันการสร้าง instance จากภายนอก private ConfigurationManager() { _settings = LoadSettingsFromDisk(); }
public static ConfigurationManager Instance => _lazyInstance.Value;
public string Get(string key) => _settings.TryGetValue(key, out var v) ? v : string.Empty;
private static Dictionary<string, string> LoadSettingsFromDisk() { // จำลองการอ่าน file config ครั้งเดียวตอนสร้าง instance return new Dictionary<string, string> { ["Env"] = "Production" }; }}
// การใช้งาน — ผูกติดกับ static member โดยตรง ทดสอบยาก mock ไม่ได้var env = ConfigurationManager.Instance.Get("Env");// ✓ ทางเลือกที่ดีกว่า — ประกาศ dependency ชัดเจน ให้ DI container คุม lifetime แทนpublic interface IConfiguration{ string Get(string key);}
public sealed class ConfigurationService : IConfiguration{ private readonly Dictionary<string, string> _settings;
public ConfigurationService(IConfigurationSource source) { _settings = source.Load(); // dependency ถูกฉีดเข้ามาอย่างชัดเจน ไม่ใช่ static cling }
public string Get(string key) => _settings.TryGetValue(key, out var v) ? v : string.Empty;}
// จุด composition root: บอก container ให้คืน instance เดียวตลอดอายุ app// services.AddSingleton<IConfiguration, ConfigurationService>();
public sealed class ReportGenerator{ private readonly IConfiguration _config;
// รับ dependency ผ่าน constructor — mock ใน unit test ได้ทันที public ReportGenerator(IConfiguration config) => _config = config;
public string BuildHeader() => $"Report ({_config.Get("Env")})";}เมื่อไหร่ควรใช้
หัวข้อที่มีชื่อว่า “เมื่อไหร่ควรใช้”- ต้องการทรัพยากรที่มีตัวตนเดียวจริง ๆ ในเชิง domain เช่น handle ของ hardware device, connection pool กลาง หรือ cache ระดับ application
- ทำงานในบริบทที่ไม่มี DI container ให้ใช้ (เช่น library ขนาดเล็กมาก หรือ script เดี่ยว ๆ) และยอมรับข้อเสียของ static cling ได้
- ต้องการ lazy initialization ของทรัพยากรที่สร้างแพง และมั่นใจว่าเข้าถึงจาก thread เดียว หรือใช้
Lazy<T>จัดการ thread-safety ให้ - ใช้เป็นกลไกภายในของ framework/infrastructure layer ที่ปิดเป็น implementation detail ไม่รั่วไหลออกไปให้ code domain ต้องรู้จัก static member โดยตรง
เมื่อไหร่ไม่ควรใช้
หัวข้อที่มีชื่อว่า “เมื่อไหร่ไม่ควรใช้”- เมื่อมี DI container ใน project อยู่แล้ว — ควรลงทะเบียน lifetime แบบ singleton ใน container แทนการ hardcode ไว้ในตัว type เอง
- เมื่อ class นั้นต้องถูก unit test แยกหน่วย เพราะ static
Instanceทำให้ mock/stub แทนไม่ได้ ทดสอบแบบแยก (isolated test) แทบเป็นไปไม่ได้ - เมื่อ state ที่ใช้ร่วมกันอาจต้องมีได้มากกว่าหนึ่งชุดในอนาคต (เช่น multi-tenant, parallel test run) เพราะ Singleton บีบให้เหลือได้แค่ตัวเดียวเสมอ
- เมื่อ code ที่พึ่งพา Singleton อยู่ลึกในหลายชั้นของระบบ เพราะ dependency ที่ซ่อนอยู่ (hidden dependency) ทำให้ยากต่อการมองเห็นว่า class ไหนพึ่งพาอะไรบ้างจาก signature
ข้อดีและข้อเสีย
หัวข้อที่มีชื่อว่า “ข้อดีและข้อเสีย”| ด้าน | รายละเอียด |
|---|---|
| ข้อดี | รับประกันว่ามี instance เดียวจริง ๆ ตลอดอายุ app |
| ข้อดี | รองรับ lazy initialization — สร้างเมื่อถูกใช้งานจริงเท่านั้น |
| ข้อดี | เข้าถึงได้จากทุกที่โดยไม่ต้องส่งต่อ reference ผ่านหลายชั้น |
| ข้อเสีย | ละเมิด Single Responsibility Principle — ผูกหน้าที่ปกติกับหน้าที่คุม lifetime |
| ข้อเสีย | เกิด static cling ทำให้ mock/ทดสอบยาก |
| ข้อเสีย | ซ่อน dependency ที่แท้จริงของ class ไว้ ไม่ปรากฏใน constructor signature |
| ข้อเสีย | ต้องจัดการ thread-safety เองถ้าไม่ใช้เครื่องมือช่วยอย่าง Lazy<T> |
ที่เกี่ยวข้อง
หัวข้อที่มีชื่อว่า “ที่เกี่ยวข้อง”- Single Responsibility
- Explicit Dependencies
- Dependency Injection
- Service Locator
- Static Cling (antipattern)
- Lazy Load