Singleton (ซิงเกิลตัน) — รูปแบบการสร้างที่รับประกันอินสแตนซ์เดียวของคลาสและให้จุดเข้าถึงส่วนกลางไปยังอินสแตนซ์นั้น Singleton ถูกใช้อย่างแพร่หลายในการพัฒนาโมบายล์สำหรับทรัพยากรที่ใช้ร่วมกัน: ไคลเอ็นต์เครือข่าย ฐานข้อมูล ตัวจัดการการตั้งค่า รูปแบบนี้ถูกอธิบายในหนังสือคลาสสิก GoF (1994) และยังคงเป็นหนึ่งในรูปแบบที่เป็นที่รู้จักมากที่สุด เรียนรู้เพิ่มเติมที่ Refactoring Guru: Singleton
ประเด็นสำคัญ
Singleton — รูปแบบการออกแบบเชิงสร้างที่อธิบายโดย GoF (Gang of Four) ในปี 1994 รูปแบบนี้แก้ปัญหาสองอย่าง: มันจำกัดการสร้างอินสแตนซ์ของคลาสให้เหลือเพียงวัตถุเดียวและให้การเข้าถึงส่วนกลางไปยังวัตถุนั้น Singleton มีประโยชน์สำหรับทรัพยากรที่ต้องไม่ซ้ำกัน: โรงงานเซสชัน, แคชรูปภาพ, ตัวจัดการการเชื่อมต่อฐานข้อมูล, ไคลเอ็นต์ Crashlytics หรือ Analytics
การนำ Singleton ไปใช้ ต้องการคอนสตรัคเตอร์ส่วนตัว (ป้องกันการสร้างจากภายนอก), ฟิลด์แบบ static พร้อมอินสแตนซ์เดียว, และเมธอดการเข้าถึงแบบ static (shared, instance, getInstance) ไคลเอ็นต์เรียก Singleton.shared.method() โดยไม่ต้องกังวลเกี่ยวกับการสร้างวัตถุ รูปแบบนี้เป็นที่นิยมใน iOS และ Android: URLSession.shared, UserDefaults.standard, FirebaseApp.sharedInstance — ทั้งหมดคือ Singleton อย่างไรก็ตาม การใช้ Singleton มากเกินไปนำไปสู่รูปแบบตรงข้าม Global State
ปัญหา Singleton — การพึ่งพาที่ซ่อนเร้น (คลาสพึ่งพาวัตถุ Singleton อย่างโดยปริยาย), ความซับซ้อนในการทดสอบ (ไม่สามารถแทนที่อินสแตนซ์ในการทดสอบได้โดยไม่ต้องใช้ความพยายามเพิ่มเติม), การละเมิดหลักการความรับผิดชอบเดียว (Singleton จัดการทั้งอินสแตนซ์ของตัวเองและตรรกะทางธุรกิจ) การพัฒนาโมบายล์สมัยใหม่นิยม DI (Dagger, Hilt, Swinject) สำหรับจัดการอินสแตนซ์เดียว — คอนเทนเนอร์ DI สร้างวัตถุหนึ่งครั้งและฉีดผ่านคอนสตรัคเตอร์
Swift Singleton ถูกนำไปใช้ผ่านพรอพเพอร์ตี้ static shared พร้อมตัวเริ่มต้นส่วนตัว ตั้งแต่ Swift 3 เป็นต้นมา การเริ่มต้นแบบขี้เกียจของพรอพเพอร์ตี้แบบ static ถูกรับประกันว่าปลอดภัยต่อเธรด — คอมไพเลอร์เพิ่มการซิงโครไนซ์โดยอัตโนมัติผ่าน dispatch_once เพียงแค่ประกาศ static let shared = Class() และทำให้ init() เป็นส่วนตัวก็เพียงพอ Swift ไม่ต้องการการซิงโครไนซ์เพิ่มเติมสำหรับการเข้าถึงแบบเธรดเดียวหลังจากการเริ่มต้น
final class NetworkManager {
// Singleton ที่ปลอดภัยต่อเธรด
static let shared = NetworkManager()
private init() {
URLSessionConfiguration.default.timeoutIntervalForRequest = 30
}
private var cache = NSCache<NSString, NSData>()
func fetchData(from url: URL) async throws -> Data {
let key = url.absoluteString as NSString
if let cached = cache.object(forKey: key) {
return cached as Data
}
let (data, _) = try await URLSession.shared.data(from: url)
cache.setObject(data as NSData, forKey: key)
return data
}
}
// การใช้งาน
let data = try await NetworkManager.shared.fetchData(from: url)
Apple Singleton — วัตถุ iOS SDK จำนวนมากใช้ Singleton: UIApplication.shared, UIScreen.main, FileManager.default, NotificationCenter.default, UserDefaults.standard Apple ใช้ Singleton สำหรับบริการที่ไม่ซ้ำกันทางกายภาพ (หน้าจอเดียว, แอปพลิเคชันเดียว) นักพัฒนาคัดลอกรูปแบบนี้สำหรับบริการของตนเอง ใน SwiftUI การเข้าถึงส่วนกลางไปยัง Singleton ถูกแทนที่ด้วย Environment และ @EnvironmentObject ซึ่งช่วยปรับปรุงความสามารถในการทดสอบ
Kotlin Singleton — วิธีที่ง่ายที่สุด: คำสำคัญ object ประกาศคลาสซิงเกิลตันพร้อมการเริ่มต้นแบบขี้เกียจเมื่อเข้าถึงครั้งแรก Kotlin object ปลอดภัยต่อเธรดและไม่ต้องการการซิงโครไนซ์เพิ่มเติม หากต้องการ Singleton พร้อมพารามิเตอร์ของคอนสตรัคเตอร์ จะใช้ companion object พร้อมตัวแทน lazy ใน Android Singleton มักจำเป็นสำหรับบริบท Application และบริการที่เริ่มต้นผ่าน Application.onCreate()
// ตัวเลือก 1: object — Singleton ง่าย ๆ ไม่มีพารามิเตอร์
object AppPreferences {
private val prefs = Application.instance
.getSharedPreferences("app", Context.MODE_PRIVATE)
var isFirstLaunch: Boolean
get() = prefs.getBoolean("first_launch", true)
set(value) = prefs.edit { putBoolean("first_launch", value) }
}
// ตัวเลือก 2: companion object — Singleton พร้อมพารามิเตอร์
class ApiClient private constructor(baseUrl: String) {
companion object {
@Volatile
private var instance: ApiClient? = null
fun getInstance(baseUrl: String): ApiClient {
return instance ?: this.synchronized {
instance ?: ApiClient(baseUrl).also { instance = it }
}
}
}
fun request(endpoint: String): String { /* ... */ }
}
Singleton ของ Android SDK — บริการระบบ Android หลายอย่างนำ Singleton ไปใช้: context.getSystemService(), Room.databaseBuilder(), Retrofit.Builder() ตัวอย่างรวมถึง SharedPreferences, MediaPlayer, AudioManager ในแอปพลิเคชัน Android Singleton มักใช้สำหรับพื้นที่เก็บข้อมูล ตัวจัดการ และโรงงาน Google แนะนำให้แทนที่ Singleton ด้วย DI (Hilt, Koin) ซึ่งขอบเขต Singleton (Scope.Singleton หรือ @Singleton) ถูกจัดการโดยคอนเทนเนอร์ในขณะที่คลาสยังคงสามารถทดสอบได้
Thread safety — ข้อกำหนดที่สำคัญสำหรับ Singleton ในสภาพแวดล้อมแบบหลายเธรด หากไม่มีการซิงโครไนซ์ สองเธรดสามารถตรวจสอบ instance == null พร้อมกันและสร้างสองอินสแตนซ์ได้ วิธีแก้ไขคือล็อกระหว่างการสร้างครั้งแรกและปล่อยหลังจากการเริ่มต้น ใน Swift พรอพเพอร์ตี้แบบ static (static let) ปลอดภัยต่อเธรดตามค่าเริ่มต้น ใน Kotlin object ปลอดภัยต่อเธรด สำหรับสไตล์ Java ใน Kotlin ใช้ synchronized หรือ @Volatile + double-check locking
| ภาษา | กลไก | ความปลอดภัยต่อเธรด | การเริ่มต้นแบบขี้เกียจ |
|---|---|---|---|
| Swift | static let | dispatch_once (อัตโนมัติ) | ใช่ เมื่อเข้าถึงครั้งแรก |
| Kotlin object | การประกาศ object | ตัวเริ่มต้นคลาสปลอดภัยต่อเธรด | ใช่ เมื่อเข้าถึงครั้งแรก |
| Kotlin companion | synchronized + @Volatile | Double-checked locking | ใช่ ผ่าน lazy หรือ synchronized |
| Java | synchronized + volatile | Double-checked locking | ใช่ ใน getInstance() |
Double-checked locking — รูปแบบสำหรับการเริ่มต้นแบบขี้เกียจของ Singleton การตรวจสอบครั้งแรกโดยไม่ซิงโครไนซ์ (รวดเร็วหากอินสแตนซ์มีอยู่แล้ว) ครั้งที่สองภายใน synchronized (สร้างโดยเธรดเดียวเท่านั้น) @Volatile รับประกันการมองเห็นการเปลี่ยนแปลงสำหรับทุกเธรด หากไม่มี volatile เธรดอื่นอาจเห็นวัตถุที่สร้างบางส่วน ใน Kotlin ตัวแทน lazy พร้อม LazyThreadSafetyMode.SYNCHRONIZED นำ double-checked locking ไปใช้โดยอัตโนมัติ
Dependency Injection — ทางเลือกแทน Singleton สำหรับจัดการอินสแตนซ์เดียว คอนเทนเนอร์ DI (Dagger, Hilt, Koin, Swinject) สร้างวัตถุหนึ่งครั้งในขอบเขต Singleton และฉีดผ่านคอนสตรัคเตอร์ คลาสไม่ทราบเกี่ยวกับสถานะ Singleton ของมัน — คอนเทนเนอร์เป็นผู้ตัดสินใจ โค้ดสามารถทดสอบได้: โมดูล DI ถูกแทนที่ด้วยโมดูลจำลองในการทดสอบ ข้อดีของ DI: การพึ่งพาที่ชัดเจนในคอนสตรัคเตอร์, ความสามารถในการแทนที่, วงจรชีวิตที่เป็นหนึ่งเดียว
เมื่อใด Singleton สมเหตุสมผล — วัตถุระดับระบบ: Crashlytics, Analytics, Logging บริการเหล่านี้ถูกเริ่มต้นหนึ่งครั้งใน AppDelegate/Application และใช้ทุกที่ DI มากเกินไปสำหรับพวกเขา Singleton ยังสะดวกสำหรับแคชรูปภาพ (NSCache, Coil, Glide) ซึ่งการเข้าถึงส่วนกลางสมเหตุสมผลด้วยประสิทธิภาพ สำหรับสิ่งอื่นทั้งหมด DI ดีกว่า: ทำให้การพึ่งพามองเห็นได้ ช่วยให้การทดสอบและการปรับโครงสร้างง่ายขึ้น
แนวทางแบบผสม — Singleton ที่สามารถแทนที่ได้สำหรับการทดสอบ ใน Swift โปรโตคอล + พรอพเพอร์ตี้แบบ static ที่การทดสอบสามารถแทนที่ได้ (เช่น ผ่าน URLProtocol สำหรับ URLSession) ใน Kotlin คลาสเปิดพร้อมพรอพเพอร์ตี้ที่ฉีดได้ ซึ่งการทดสอบตั้งค่าจำลองผ่านการสะท้อนหรือ setter แนวทางนี้รักษาความเรียบง่ายของ Singleton แต่ให้ความสามารถในการทดสอบ Google แนะนำ Hilt สำหรับ Android Apple ไม่บังคับ DI สำหรับ iOS — การเลือกขึ้นอยู่กับทีม
คำถามที่พบบ่อย
ไม่ Singleton เป็นรูปแบบ GoF แต่การใช้งานที่ไม่ถูกต้องบ่อยครั้งเปลี่ยนมันเป็นรูปแบบตรงข้าม Global State Singleton สมเหตุสมผลสำหรับทรัพยากรที่ไม่ซ้ำกันทางกายภาพ (หน้าจอ, เครื่องพิมพ์, ระบบไฟล์) ปัญหาเกิดขึ้นเมื่อ Singleton ใช้สำหรับการจัดการข้อมูล: การพึ่งพาที่ซ่อนเร้น, ความซับซ้อนในการทดสอบ, การละเมิดหลักการความรับผิดชอบเดียว ทางเลือกสมัยใหม่คือ DI พร้อมขอบเขต Singleton
สามแนวทาง: (1) ผ่านโปรโตคอล — Singleton นำโปรโตคอลไปใช้, การทดสอบสลับการนำไปใช้; (2) ผ่าน DI — Singleton ถูกฉีดเป็นการพึ่งพาผ่านคอนสตรัคเตอร์; (3) ผ่านเมธอด reset — Singleton มีเมธอดสำหรับรีเซ็ตสถานะในการทดสอบ (เฉพาะบิลด์ทดสอบ) แนวทางแรกดีกว่า แนวทางที่สามอันตรายสำหรับโปรดักชัน Swift อนุญาตให้แทนที่พรอพเพอร์ตี้ shared ผ่านการจัดการในรันไทม์ในการทดสอบ
Kotlin object เป็นโครงสร้างภาษาที่สร้าง Singleton ในระดับไบต์โค้ด แตกต่างจากการนำไปใช้ของ Java ที่มีคอนสตรัคเตอร์ส่วนตัวและ getInstance() object รับประกันความปลอดภัยต่อเธรด การเริ่มต้นแบบขี้เกียจ และห้ามการสืบทอด Java Singleton ต้องการการซิงโครไนซ์ด้วยตนเอง (synchronized) และ volatile เพื่อการทำงานที่ถูกต้องในสภาพแวดล้อมแบบหลายเธรด Kotlin object เป็นวิธีที่ปลอดภัยที่สุดและกระชับที่สุดใน Android
การสืบทอด Singleton ทำลายรูปแบบ: หากคลาส Singleton สามารถสืบทอดได้ คลาสย่อยสามารถสร้างอินสแตนซ์ที่สอง ละเมิดความเป็นเอกลักษณ์ ใน Swift final class ห้ามการสืบทอด Kotlin object ไม่สามารถสืบทอดได้ (object เป็น sealed) หากต้องการ Singleton ที่มีความแปรปรวน ให้ใช้คอนเทนเนอร์ DI พร้อมขอบเขต Singleton: รับประกันอินสแตนซ์เดียวและสนับสนุนการสืบทอดผ่านอินเทอร์เฟซ
พารามิเตอร์ถูกส่งผ่าน init(context: Application) หรือ getInstance(param) Kotlin object ไม่รับพารามิเตอร์ — ใช้ companion object พร้อมเมธอดโรงงาน getInstance(param) Hilt แก้ปัญหา: @Singleton + @Inject constructor(context: Application) — คอนเทนเนอร์ DI ฉีดบริบท Application โดยอัตโนมัติ สำหรับไคลเอ็นต์ Retrofit พารามิเตอร์ (baseUrl, interceptors) ถูกส่งผ่าน builder ในโมดูล DI
สรุป
เราจะพัฒนาแอปพลิเคชันบนมือถือแบบครบวงจร
IT Sectr สร้างแอปพลิเคชัน iOS และ Android สำหรับสตาร์ทอัพและธุรกิจตั้งแต่ปี 2017 เราจะให้คำแนะนำและเสนอวิธีแก้ปัญหาที่ดีที่สุดแก่คุณ
อ่านเพิ่มเติม