ข้าม​ไป​ยัง​เนื้อหา

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 ให้​อัตโนมัติ

// รูปแบบคลาสสิกตาม GoF — thread-safe ด้วย Lazy<T> แต่ยังเป็น static cling
public 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>