1. تعریف و هدف Singleton
Singleton یه الگوی طراحی است که تضمین میکنه یک کلاس فقط یک نمونه (instance) داشته باشه و یه نقطه دسترسی سراسری (global access point) به اون نمونه فراهم کنه.
2. چرا Singleton مهم است؟
گاهی نیاز داریم که فقط یه نسخه از یک کلاس ساخته بشه، مثلاً مدیریت اتصال دیتابیس یا تنظیمات کلی برنامه.
جلوگیری از مصرف اضافی حافظه و مشکلاتی مثل ناسازگاری دادهها.
تسهیل کنترل روی منبعی که باید یکپارچه و یکتا باشه.
3. چطوری کار میکنه؟
سازنده (constructor) کلاس را private یا protected میکنیم تا از ساخت نمونه جدید جلوگیری کنیم.
یک متد استاتیک برای دسترسی به نمونهی واحد کلاس ایجاد میکنیم.
اولین بار که این متد صدا زده شود، نمونه ساخته میشود و در دفعات بعدی همان نمونه بازگردانده میشود.
چرا از Singleton استفاده میکنیم؟
جلوگیری از ایجاد چندین شیء از یک کلاس خاص.
کنترل دقیق روی منابع مشترک (مثل اتصال پایگاه داده، تنظیمات اپلیکیشن، یا کلاسهای کش).
حفظ وضعیت (state) در طول عمر برنامه در جاهایی که نیازه.
class Logger
{
private static $instance = null;
private function __construct()
{
// اتصال به فایل لاگ یا هر چیز دیگر
}
public static function getInstance()
{
if (!self::$instance) {
self::$instance = new Logger();
}
return self::$instance;
}
public function log($message)
{
echo "[LOG]: " . $message . PHP_EOL;
}
}
راه استفاده:
$logger1 = Logger::getInstance();
$logger2 = Logger::getInstance();
$logger1->log("Hello Singleton!");
var_dump($logger1 === $logger2); // true
در این مثال، وقتی کلاس Singleton
ساخته میشود، اگر قبلاً نمونهای ساخته شده باشد، همان نمونه قبلی برگردانده میشود.
مثال Singleton در PHP (لاراولی)
class DatabaseConnection {
private static $instance = null;
private function __construct() {
// اتصال به دیتابیس
}
public static function getInstance() {
if (self::$instance === null) {
self::$instance = new DatabaseConnection();
}
return self::$instance;
}
}
// استفاده
$db1 = DatabaseConnection::getInstance();
$db2 = DatabaseConnection::getInstance();
var_dump($db1 === $db2); // bool(true)
گر در محیطهایی که چندین ترد (thread) دارند کار میکنید، باید مراقب باشید که ایجاد نمونه به صورت thread-safe انجام شود.
گاهی اوقات Singleton میتواند باعث ایجاد وابستگی زیاد و سختی در تست کد شود؛ بنابراین باید با احتیاط استفاده شود.
در زبانهایی مثل جاوا یا سیشارپ، به صورت رسمی و ساختیافتهتر توسط زبان پشتیبانی میشود.
7. موارد استفاده رایج Singleton
اتصال دیتابیس (Database connection pool)
Logger (ثبت وقایع برنامه)
Cache manager
تنظیمات کلی برنامه (Configuration manager)
8. چرا بعضی وقتها Singleton بد است؟
گاهی میتواند به Anti-pattern تبدیل شود و به برنامه وابستگی غیرضروری اضافه کند.
تستپذیری را سخت میکند چون نمونهها سخت جایگزین میشوند.
استفاده زیاد از Global State در برنامه که باعث پیچیدگی و خطا میشود.
کاربردهای Singleton کجاهاست؟
1. مدیریت اتصال به دیتابیس (Database Connection)
وقتی برنامه نیاز داره فقط یک اتصال ثابت به دیتابیس داشته باشه.
اگر هر بار بخواهیم یه اتصال جدید بسازیم، منابع زیادی مصرف میشه.
Singleton تضمین میکنه که فقط یک نمونه اتصال ساخته و مجددا استفاده بشه.
2. سیستم ثبت گزارش (Logging)
مثلا یک Logger که کل برنامه ازش استفاده میکنه تا خطاها و وقایع رو ذخیره کنه.
نیاز داریم که همهجا به یک Logger واحد دسترسی داشته باشند تا گزارشها یکپارچه و سازماندهی شده باشن.
3. مدیریت تنظیمات برنامه (Configuration Manager)
تنظیمات برنامه معمولا باید یکجا ذخیره و مدیریت بشن.
Singleton کمک میکنه تا همه بخشهای برنامه از یک منبع مشترک و ثابت استفاده کنن.
4. کش (Cache)
وقتی میخوایم دادههایی که محاسبه یا واکشی شده رو ذخیره کنیم و در جای جای برنامه بهش دسترسی داشته باشیم.
Singleton به عنوان Cache Manager میتونه کار کنه.
5. کنترل دسترسی به سختافزار یا منابع مشترک
مثلا وقتی فقط یک چاپگر یا اتصال به دستگاه خاصی هست و باید ازش به صورت یکتا استفاده کنیم.
6. مدیریت Thread Pool یا Connection Pool
برای بهینهسازی استفاده از منابع در برنامههای چند نخی یا برنامههایی که به تعداد زیادی اتصال نیاز دارند.
خوب الان با هم بریم یه مثال عملی از پیادهسازی حرفهای و واقعی از Singleton Pattern برای ارسال SMS توی پروژه لاراول انجام بدیم.
- ساخت کلاس SmsService
- تعریف آن بهصورت Singleton در AppServiceProvider
- استفاده از آن در کنترلر یا جای دیگه
namespace App\Services;
class SmsService
{
protected $client;
public function __construct()
{
$this->client = new \SomeSms\Sdk([
'api_key' => env('SMS_API_KEY'),
'sender' => env('SMS_SENDER'),
]);
}
public function send(string $phone, string $message): bool
{
return $this->client->send($phone, $message);
}
}
2. تعریف سرویس بهصورت Singleton در Service Container
در AppServiceProvider.php، در متد register() اینو اضافه کن:
use App\Services\SmsService;
public function register(): void
{
$this->app->singleton(SmsService::class, function ($app) {
return new SmsService();
});
}
استفاده از سرویس در کنترلر
use App\Services\SmsService;
class UserController extends Controller
{
public function sendOtp(Request $request, SmsService $sms)
{
$phone = $request->input('phone');
$otp = rand(1000, 9999);
$sms->send($phone, "کد تایید شما: $otp");
return response()->json(['status' => 'ok']);
}
}
اینم از env فایل
SMS_API_KEY=your-api-key-here
SMS_SENDER=30001234
حالا برگدیم به کنترلرمون و متد sendotp بررسی کنیم بیشتر
public function sendOtp(Request $request, SmsService $sms)
لاراول وقتی بخواد این متد رو اجرا کنه، اول میبینه که چه چیزی نیاز داره (Request, SmsService).
چون SmsService در AppServiceProvider بهصورت singleton رجیستر شده:
$this->app->singleton(SmsService::class, function ($app) {
return new SmsService();
});
لاراول متوجه میشه که چطور باید یک نسخه از SmsService بسازه یا بازیابی کنه.
بنابراین، بدون اینکه شما new SmsService() صدا بزنی، لاراول خودش هندل میکنه و اون instance رو به متد تزریق میکنه.