Singleton (تکی) — الگوی ساختی که نمونه یگانه کلاس را تضمین میکند و نقطه دسترسی سراسری به آن فراهم میکند. Singleton به طور گسترده در توسعه موبایل برای منابع مشترک استفاده میشود: کلاینتهای شبکه، پایگاههای داده، مدیران تنظیمات. این الگو در کتاب کلاسیک GoF (1994) توصیف شده و یکی از شناختهشدهترینها باقی مانده است. بیشتر — در Refactoring Guru: Singleton.
نکات اصلی
Singleton (تکی) — الگوی طراحی ساختی که توسط GoF (Gang of Four) در سال 1994 توصیف شده است. این الگو دو مسئله را حل میکند: ایجاد نمونه کلاس را به یک شی محدود میکند و دسترسی سراسری به آن شی فراهم میکند. Singleton برای منابعی که باید یکتا باشند مفید است: کارخانه نشست، حافظه پنهان تصاویر، مدیر اتصال پایگاه داده، کلاینت Crashlytics یا Analytics.
پیادهسازی Singleton نیاز به سازنده خصوصی (جلوگیری از ایجاد خارجی)، فیلد ایستا با نمونه یگانه و متد دسترسی ایستا (shared، instance، getInstance) دارد. کلاینتها Singleton.shared.method() را بدون نگرانی از ایجاد شی فراخوانی میکنند. این الگو در iOS و Android محبوب است: URLSession.shared، UserDefaults.standard، FirebaseApp.sharedInstance — همه اینها Singleton هستند. با این حال استفاده بیش از حد Singleton به ضدالگوی Global State منجر میشود.
مشکلات Singleton — وابستگیهای پنهان (کلاسها به طور ضمنی به شی Singleton وابسته هستند)، دشواری تستنویسی (نمیتوان نمونه را در تست بدون تلاش اضافی جایگزین کرد)، نقض Single Responsibility Principle (Singleton هم نمونه خود و هم منطق کسبوکار را مدیریت میکند). توسعه موبایل مدرن DI (Dagger، Hilt، Swinject) را برای مدیریت نمونههای یگانه ترجیح میدهد — کانتینر DI شی را یک بار ایجاد میکند و از طریق سازنده تزریق میکند.
Swift Singleton از طریق ویژگی ایستای shared با مقداردهنده اولیه خصوصی پیادهسازی میشود. از Swift 3، مقداردهی تنبل ویژگیهای ایستا به طور تضمینی نخایمن است — کامپایلر به طور خودکار همگامسازی را از طریق dispatch_once اضافه میکند. کافی است static let shared = Class() اعلام کرده و init() را خصوصی کنید. Swift بعد از مقداردهی برای دسترسی تکنخی به همگامسازی اضافی نیاز ندارد.
final class NetworkManager {
// Thread-safe 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 { /* ... */ }
}
Android SDK Singleton — بسیاری از خدمات سیستمی 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 let) به طور پیشفرض نخایمن هستند. در Kotlin object نخایمن است. برای سبک Java در Kotlin از synchronized یا @Volatile + double-check locking استفاده میشود.
| زبان | مکانیزم | نخایمنی | مقداردهی تنبل |
|---|---|---|---|
| Swift | static let | dispatch_once (خودکار) | بله، در اولین دسترسی |
| Kotlin object | Object declaration | مقداردهنده کلاس نخایمن است | بله، در اولین دسترسی |
| 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 با ماژول mock جایگزین میشود. مزایای DI: وابستگیهای صریح در سازنده، امکان بازنویسی، چرخه حیات یکپارچه.
چه موقع Singleton توجیه دارد — اشیای سطح سیستم: Crashlytics، Analytics، Logging. این خدمات یک بار در AppDelegate/Application مقداردهی میشوند و در همه جا استفاده میشوند. DI برای آنها بیش از حد است. Singleton همچنین برای حافظههای پنهان تصاویر (NSCache، Coil، Glide) راحت است، جایی که دسترسی سراسری با عملکرد توجیه میشود. برای بقیه موارد DI ترجیح داده میشود: وابستگیها را آشکار میکند، تستنویسی و بازسازی را ساده میکند.
رویکرد ترکیبی — Singleton با امکان بازنویسی برای تستها. در Swift از پروتکل + ویژگی ایستا استفاده میشود که تست میتواند جایگزین کند (مثلاً از طریق URLProtocol برای URLSession). در Kotlin — کلاس باز با ویژگی تزریقپذیر، که تست از طریق بازتاب یا setter mock را تنظیم میکند. چنین رویکردی سادگی Singleton را حفظ میکند اما ابزارهایی برای تستنویسی فراهم میکند. Google Hilt را برای Android توصیه میکند، Apple DI را برای iOS تحمیل نمیکند — انتخاب به تیم بستگی دارد.
سوالات متداول
خیر، Singleton الگوی GoF است، اما استفاده نادرست مکرر آن را به ضدالگوی Global State تبدیل میکند. Singleton برای منابع فیزیکی یکتا (صفحه، چاپگر، سیستم فایل) توجیه دارد. مشکلات زمانی ایجاد میشوند که Singleton برای مدیریت دادهها استفاده میشود: وابستگیهای پنهان، دشواری تستنویسی، نقض Single Responsibility Principle. جایگزین مدرن — DI با محدوده Singleton.
سه رویکرد: (1) از طریق پروتکل — Singleton پروتکل را پیادهسازی میکند، تستها پیادهسازی را جایگزین میکنند؛ (2) از طریق DI — Singleton به عنوان وابستگی از طریق سازنده تزریق میشود؛ (3) از طریق متد بازنشانی — Singleton متدی برای بازنشانی حالت در تستها دارد (فقط برای بیلد تست). رویکرد اول ترجیح داده میشود، سوم — برای تولید خطرناک است. Swift اجازه میدهد ویژگی shared را از طریق دستکاری runtime در تستها جایگزین کنید.
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 از سال 2017 برنامههای iOS و Android را برای استارتاپها و کسبوکارها ایجاد میکند. ما به شما مشاوره میدهیم و بهترین راهحل را پیشنهاد خواهیم کرد.
همچنین بخوانید