بررسی کاربرد و طرز کار دیزاین پترن singleton

در هنگام برنامه‌نویسی شی‌گرا، موارد زیادی پیش می‌آید که ما فقط و فقط به یک نمونه از یک کلاس خاص احتیاج پیدا می‌کنیم. به عنوان مثال فرض کنید برای یک بازی کلاسی نوشته‌اید تا صحنه را مدیریت کند و بر سایر اجزا نظارت داشته باشد. بدیهی است که ما فقط به یک شی از این کلاس احتیاج خواهیم داشت و ساختن بیش‌تر از یک شی موجب هدررفت حافظه یا حتی کرش کردن برنامه می‌شود. در این موارد باید سیستمی ایجاد کنیم که اجازه ندهد بیشتر از یک شی از روی کلاس موردنظر ما ساخته شود.

به طور ساده‌تر باید الگوریتمی نوشته شود که هنگام نیاز به شی از روی کلاس فوق اگر از روی کلاس شی‌ای ساخته نشده باشد دستور ساختن یک شی اجرا شود و اگر شی‌ای قبلاً ساخته شده است همان شی ساخته شده برگردانده شود و شی جدیدی ساخته نشود.

یکی از استانداردترین راه‌های ساخت چنین منطقی استفاده از دیزاین پترن Singleton است. در این دیزاین پترن درون کلاسی که قصد داریم تنها یک بار از روی آن نمونه‌سازی شود یک فیلد static از جنس خودش تعریف می‌کنیم. معمولاً نام این فیلد را instance می‌گذارند.

بعد از انجام این کار در اولین اجرای کد با استفاده از دستور new فیلد instance مقداردهی می‌شود و در دفعات بعدی (که با چک کردن null نبودن instance از مقداردهی شدن آن مطلع می‌شویم) تنها شی instance بازگردانی می‌شود.

دیزاین پترن Singleton یکی از مواردی است که ابهامات زیادی را به وجود می‌آورد. چرا که تعریف سنتی آن یعنی «الگویی که در آن می‌توان از یک کلاس تنها یک شی ساخت» چندان صحیح نیست و باید گفت اصولاً در دیزاین پترن سینگلتون هدف این هست که نتوان از روی آن کلاس بیشتر از یک شی ساخت اما این عمل با استفاده از ایجاد یک «اشاره‌گر» static از نوع کلاس موردنظر حاصل می‌شود. نه این که امکان نمونه‌سازی از کل کلاس از بین برود.

کلاس زیر که ساده‌ترین پیاده‌سازی سینگلتون (lazy instantiation) است را در نظر بگیرید:

class Singleton
{
public int TotalCoins;
public int TotalGems;
private static Singleton instance;
public static Singleton Instance
{
get
{
if (instance == null)
instance = new Singleton();
return instance;
}
}
}

برای مقداردهی به شی instance در هر کجای کد می‌توان به صورت زیر عمل کرد:

Singleton.Instance.TotalCoins = 20;
Singleton.Instance.TotalGems = 5;

اگر مبحث برنامه‌نویسی چندنخی را در نظر نگیریم (چرا که lazy instantiation ساده است و thread-safe نیست)، فیلد instance تنها یک بار نمونه‌سازی می شود و در دفعات بعد صرفا به آن اشاره می‌شود. هر چند اشاره‌گر static است اما شی‌ای که به آن اشاره می‌کند dynamic است و static بودن اشاره‌گر آن دلیلی بر تفاوت شی اشاره شده با یک شی معمولی ندارد.

پس: یکتا بودن شی به دلیل این است که تنها یک بار ساخته می‌شود و static بودن محدود به اشاره‌گر شی می‌شود. چون اساساً شی static معنی ندارد.

شاید بیشتر این ابهام به دلیل این باشد که برای جلوگیری از اسپاگتی شدن کد (به معنی نامنظم و سخت شدن فهم کد) این تعریف در بدنه‌ی خود کلاس صورت می‌پذیرد. اگر فیلدهای سینگلتون در کلاسی جداگانه تعریف شوند شاید فهم آن ساده‌تر شود:

class Singleton
{
public int TotalCoins;
public int TotalGems;
}
class SingletonHolder
{
private static Singleton instance;
public static Singleton Instance
{
get
{
if (instance == null)
instance = new Singleton();
return instance;
}
}
}

بعد از این توضیح باید به چهار سوال پاسخ داد:

1- پس امکان نمونه‌سازی از کلاسی که در آن دیزاین پترن سینگلتون پیاده‌سازی شده وجود دارد؟

بله، قطعا هیچ دلیلی برای کار نکردن قطعه کد

Singleton secondSingleton = new Singleton();
secondSingleton.TotalCoins = 100;
secondSingleton.TotalGems = 50;

وجود ندارد و فیلدهای secondSingleton می‌توانند فارغ از نمونه‌ی Instance مقداردهی شوند و خوشبختانه دسترسی به فیلد Instance از طریق شی secondSingleton وجود ندارد. چرا که آن فیلد static است و امکان دسترسی به آن از طریق شی (در سی شارپ) وجود ندارد. هر چند که با اعمالی نظیر تعریف یک متد constructor با سطح دسترسی private می‌توان از امکان نمونه‌سازی کلاس جلوگیری کرد اما ربط چندانی به دیزاین پترن سینگلتون ندارد.

2- امکان تعریف متد در این کلاس وجود دارد و اگر این امکان هست مقادیری که متد از فیلدها باز می‌گرداند مربوط به نمونه‌ی instance هستند و یا نمونه‌ای که به صورت دستی ساخته شده؟

بله. امکان تعریف متد در کلاس وجود دارد. متد فرضی زیر را در نظر بگیرید:

public int returnTotalGems()
{
return TotalGems;
}

اگر متد از طریق Instance صدا زده شود یعنی:

Console.WriteLine(
Singleton.Instance.returnTotalGems());

فیلد Instance و اگر از طریق شی دست‌ساز صدا شود یعنی:

Console.WriteLine(
secondSingleton.returnTotalGems());

فیلد شی را برمی‌گرداند.

3- گفته شد که در سی شارپ امکان دسترسی به فیلد استاتیک توسط شی وجود ندارد. اما این امکان در زبان جاوا وجود دارد. آیا این باعث ایجاد خطا نمی‌شود؟

این یک تناقض در زبان جاوا با مفاهیم شی‌گرایی است. اما آن گونه که به نظر می‌رسد نیست و در جاوا هم این امکان وجود ندارد (چرا که اگر وجود داشت موجب ایجاد بی‌نهایت Instance و کرش کردن می‌شد). هر چند جاوا این امکان را می‌دهد که از طریق شی بتوان به فیلد static دسترسی داشت اما javac (کامپایلر جاوا) آن شی را کلاسی از نوع شی در نظر می‌گیرد. البته بهتر است که در جاوا هم این کار را انجام ندهیم.

4- آیا نمی‌شد همین کارها را با استفاده از یک کلاس static انجام می‌دادیم؟

در اینجا تفاوتهایی از این دو را بررسی می‌کنیم:

  • شی سینگلتون در حافظه‌ی heap ذخیره می‌شود (چرا که شی است)، اما کلاس static در حافظه‌ی stack که می‌تواند باعث تاثیر در پرفورمنس شود.

  • امکان ساخت شی از روی کلاس سینگلتون وجود دارد (که در سوال اول بررسی کردیم)، اما امکان ساخت شی از روی کلاس static وجود ندارد.

  • و اما مهم‌ترین تفاوت: کلاس سینگلتون می‌تواند اینترفیس implement کند و یا در ارث‌بری شرکت کند. اما این امکان برای کلاس‌های static وجود ندارد.