توضیح دوات: برای راهنمایی و اطلاعات بیشتر در مورد نمودار کلاس به این مقاله مراجعه کنید.
الگوی طراحی Singleton یک الگوی طراحی از نوع creational (ایجادی) است که به ما امکان میدهد مطمئن شویم یک کلاس فقط یک نمونه دارد و همزمان یک نقطه دسترسی جهانی به این نمونه فراهم میکند. الگوی Singleton روشی استاندارد برای ارائه یک نمونه خاص از یک عنصر در سراسر دوره حیات یک برنامه فراهم میآورد. به عبارتی دیگر، اگر برنامه مجدداً راهاندازی نشود، این نمونه بدون تغییر باقی میماند. میتوانید کد نمونه این پست را در GitHub پیدا کنید.
درک مسئله
الگوی Singleton دو مسئله را بهطور همزمان حل میکند.
- اطمینان از اینکه یک کلاس تنها یک نمونه دارد. تصور کنید که در حال پیادهسازی یک راهحل کشینگ جدید هستیم. کلاسی به نام
CachingProvider
داریم که شامل یک دیکشنری است. در این دیکشنری، تمام ورودیهای کش خود را ذخیره میکنیم. پس از مدتی تصمیم میگیریم که یک نمونه جدید ازCachingProvider
ایجاد کنیم. به جای دریافت یک شیء جدید، نیاز داریم که به همان شیء ایجادشده قبلی دسترسی پیدا کنیم. این رفتار با یک سازنده معمولی قابل اجرا نیست، زیرا به طور ذاتی تمام فراخوانیهای سازنده باید همیشه یک شیء جدید را برگردانند. - فراهم کردن یک نقطه دسترسی جهانی به نمونه. درحالیکه متغیرهای عمومی میتوانند بسیار مفید باشند، اما بسیار ناایمن نیز هستند؛ چرا که هر کدی میتواند محتوای این متغیرها را بازنویسی کرده و برنامه را دچار مشکل کند. مانند متغیرهای عمومی، الگوی Singleton امکان دسترسی به برخی اشیاء از هر نقطه در برنامه را فراهم میکند. با این حال، از بازنویسی آن نمونه توسط سایر توابع محافظت میکند.
تمام پیادهسازیهای الگوی Singleton دو مرحله مشترک دارند:
- سازنده پیشفرض را خصوصی کنید تا از استفاده از عملگر
new
توسط سایر اشیاء با کلاس Singleton جلوگیری شود. - یک روش ایجاد کننده استاتیک بسازید که بهعنوان سازنده عمل کند. در پشت صحنه، این روش سازنده خصوصی را فراخوانی کرده و شیء را در یک فیلد استاتیک ذخیره میکند. تمام فراخوانیهای بعدی به این روش، شیء کششده را برمیگرداند.
اگر کد به کلاس Singleton دسترسی داشته باشد، میتواند متد استاتیک Singleton را فراخوانی کند. بنابراین هرجا که این متد فراخوانی شود، همیشه همان شیء بازگردانده میشود.
ساختاردهی الگوی Singleton
نمودار زیر نشان میدهد که الگوی Singleton چگونه کار میکند.
کلاس Singleton دقیقا یک نمونه از خود را تعریف میکند و این نمونه بهطور جهانی در دسترس است.
در پیادهسازی پایه، الگوی Singleton یک شرکتکننده دارد:
Singleton: کلاس Singleton متد استاتیک GetInstance
را اعلام میکند که همان نمونه از کلاس خود را برمیگرداند.
توضیح دوات: راهنمای نمودار کلاس را مشاهده کنید.
برای نشان دادن نحوه کار الگوی Singleton، مثالی از زنگ کوچکی که روی پیشخوان رستورانها قرار دارد را در نظر میگیریم.
موضوع این زنگ این است که احتمالاً فقط یکی از آنها وجود دارد؛ صدای آن برای آگاهسازی پیشخدمتها است که سفارش بعدی در دسترس بوده و نیاز است که به میزها منتقل شود. از آنجا که همیشه تنها یک زنگ وجود دارد، میتوانیم آن را به عنوان یک Singleton مدلسازی کنیم.
namespace Singleton.NaiveSingleton
{
///
/// Singleton ساده
///
public sealed class Bell
{
private static Bell instance = null;
// سازنده خصوصی
private Bell() { }
// ویژگی برای دسترسی به نمونه
public static Bell Instance
{
get
{
// بررسی اینکه آیا نمونه قبلاً ایجاد شده است
if (instance == null)
instance = new Bell();
return instance;
}
}
public void Ring()
{
Console.WriteLine("Ding! Order Up!");
}
}
}
توجه داشته باشید که کلاس Bell
یک سازنده خصوصی دارد. این کار برای اطمینان از این است که نمیتواند مستقیماً ایجاد شود و فقط از طریق ویژگی Instance
قابل دسترسی است.
بیایید متد Main
را تنظیم کنیم. ابتدا باید یک متغیر جدید برای زنگ تعریف کنیم. برای ایجاد یک نمونه از کلاس NaiveSingleton
باید ویژگی Instance
آن را فراخوانی کنیم.
using Singleton.NaiveSingleton;
Bell bell = Bell.Instance;
bell.Ring();
و خروجی کد بالا به صورت زیر خواهد بود:
Ding! Order up!
انواع روش های پیادهسازی الگوی Singleton
روشهای مختلفی برای پیادهسازی الگوی طراحی Singleton وجود دارد.
Singleton ساده (بدون ایمنی در برابر چندنخی)
namespace Singleton.NaiveSingleton
{
///
/// Singleton ساده
///
public sealed class NaiveSingleton
{
private static NaiveSingleton instance = null;
// سازنده خصوصی
private NaiveSingleton() { }
// ویژگی برای دسترسی به نمونه
public static NaiveSingleton Instance
{
get
{
// بررسی اینکه آیا نمونه قبلاً ایجاد شده است
if (instance == null)
instance = new NaiveSingleton();
return instance;
}
}
public void Ring()
{
Console.WriteLine("Ding! Order Up!");
}
}
}
این پیادهسازی ایمنی در برابر چندنخی را تضمین نمیکند زیرا دو رشته مختلف میتوانند متغیر instance
را به صورت null
بخوانند و نمونههای مختلفی ایجاد کنند، در نتیجه تضمین Singleton برای یک نمونه جهانی را نقض میکند.
Singleton ایمن برای رشتهها
namespace Singleton.SingleThreadSafe
{
public sealed class SingleThreadSafe
{
private static SingleThreadSafe instance = null;
private static object lockpad = new object();
// سازنده خصوصی
private SingleThreadSafe() { }
// ویژگی برای دسترسی به نمونه
public static SingleThreadSafe Instance
{
get
{
// قفل کردن شیء مشترک
lock (lockpad)
{
if(instance == null)
instance = new SingleThreadSafe();
return instance;
}
}
}
public void Ring()
{
Console.WriteLine("Ding! Order Up!");
}
}
}
کلاس بالا تا حدی ایمنی در برابر رشتهها را فراهم میکند زیرا رشته قفل شیء مشترک را قبل از بررسی نمونه برقرار میکند. قفل کردن lockpad
، تضمین میکند که همه خواندنها پس از دریافت قفل انجام شوند و باز کردن lockpad
نیز تضمین میکند که همه نوشتنها قبل از رهاسازی قفل انجام شوند.
این تضمین میکند که تنها یک رشته، نمونه را ایجاد خواهد کرد، زیرا تنها یک رشته میتواند در هر زمان قفل را دریافت کند. عملکرد برنامه به دلیل نیاز به گرفتن و آزاد کردن padlock
هر بار که یک کلاس به نمونه ارجاع میدهد، کاهش مییابد.
Singleton با قفل دو مرحلهای
namespace Singleton.DoubleCheckLocking
{
public sealed class DoubleCheckLocking
{
private static DoubleCheckLocking instance = null;
private static object lockpad = new object();
// سازنده خصوصی
private DoubleCheckLocking() { }
public static DoubleCheckLocking Instance
{
get
{
// اولین بررسی
if(instance == null)
{
// قفل کردن قفل
lock (lockpad)
{
if(instance == null)
instance = new DoubleCheckLocking();
}
}
return instance;
}
}
public void Ring()
{
Console.WriteLine("Ding! Order Up!");
}
}
}
این پیادهسازی تلاش میکند ایمنی در برابر رشتهها را بدون قفل کردن lockpad
در هر بار فراخوانی نمونه فراهم کند. این نسخه از الگوی Singleton معایبی دارد. باید به همین شکل استفاده شود زیرا هرگونه تغییر چشمگیر در آن احتمالاً بر عملکرد یا درستی تأثیر میگذارد.
Singleton بدون قفل، ایمن در برابر رشتهها
namespace Singleton.NoLockThreadSafe
{
public sealed class NoLockThreadSafe
{
private static NoLockThreadSafe instance = new NoLockThreadSafe();
// سازنده استاتیک صریح برای اجبار به کامپایلر C#
// تا نوع را به عنوان beforefieldinit علامتگذاری نکند
static NoLockThreadSafe() { }
private NoLockThreadSafe() { }
public static NoLockThreadSafe Instance
{
get { return instance; }
}
public void Ring()
{
Console.WriteLine("Ding! Order Up!");
}
}
}
این یک پیادهسازی بسیار ساده از یک Singleton ایمن در برابر رشتهها و با ایجاد تنبل است. در C#، سازندههای استاتیک تنها زمانی اجرا میشوند که یک نمونه از کلاس ایجاد شود یا یک عضو استاتیک فراخوانی گردد. سازندههای استاتیک نیز فقط یک بار در هر AppDomain اجرا میشوند.
از آنجا که بررسی اینکه نوع تازه ساخته شده است، همواره انجام میشود، سریعتر از افزودن چکهای قفل اضافی است. اما نسبت به سایر پیادهسازیها به اندازه کافی تنبل نیست. به طور خاص، اگر اعضای استاتیک دیگری غیر از Instance
داشته باشیم، اولین ارجاع به هر یک از آنها منجر به ایجاد نمونه خواهد شد.
ایجاد تنبل کامل
namespace Singleton.FullyLazyInstantiation
{
public sealed class FullyLazyInstantiation
{
private FullyLazyInstantiation() { }
public static FullyLazyInstantiation Instance { get { return Nested.Instance; } }
private class Nested
{
// سازنده استاتیک صریح برای اجبار به کامپایلر C#
// تا نوع را به عنوان beforefieldinit علامتگذاری نکند
static Nested() { }
internal static readonly FullyLazyInstantiation Instance = new FullyLazyInstantiation();
}
public void Ring()
{
Console.WriteLine("Ding! Order Up!");
}
}
}
در این پیادهسازی، ایجاد نمونه توسط اولین ارجاع به عضو استاتیک کلاس تو در تو (Nested) انجام میشود. این تنها با فراخوانی Instance
رخ میدهد. این پیادهسازی به طور کامل تنبل است، ولی تمام مزایای عملکردی پیادهسازیهای قبلی را نیز دارد.
توجه داشته باشید که اگرچه کلاس تو در تو به اعضای خصوصی کلاس خارجی دسترسی دارد، ولی برعکس آن امکانپذیر نیست. اما این مسئله مشکلی ایجاد نمیکند، چون خود کلاس تو در تو خصوصی است.
استفاده از نوع Lazy در دات نت ۴
namespace Singleton.DotNetLazy
{
public sealed class DotNetLazy
{
private static readonly Lazy lazy =
new Lazy(() => new DotNetLazy());
public static DotNetLazy Instance => lazy.Value;
private DotNetLazy() { }
public void Ring()
{
Console.WriteLine("Ding! Order Up!");
}
}
}
اگر از نسخه .NET 4 یا بالاتر استفاده کنیم (که احتمالاً همینطور است)، میتوانیم از نوع System.Lazy
استفاده کنیم تا تنبلی ایجاد نمونه به شکل سادهتری پیادهسازی شود. تنها کافی است یک delegate به سازنده آن بدهیم که سازنده Singleton را فراخوانی کند.
این پیادهسازی از مزایای کامل تنبلی و ایمنی در برابر رشتهها بهره میبرد و برای اغلب موارد کاربردی ایدهآل است، به خصوص در مواردی که کد بهروزرسانی شده به پشتیبانی از آخرین نسخههای داتنت نیاز دارد.
مزایا و معایب الگوی Singleton
مزایا
- اطمینان از اینکه کلاس تنها یک نمونه دارد.
- دسترسی سراسری (گلوبال) به آن نمونه.
- میتوان Singlton را به صورت تنبل نمونه سازی کرد.
معایب
- بالقوه این الگو میتواند اشکالات طراحی را از دید پنهان کن، به عنوان مثال، اجزای برنامه به یکدیگر دسترسی زیادی دارند.
- در محیطهای چند نخی، پیاده سازی این الگوی طراحی نیاز به دقت دارد.
- ممکن است آزمون واحد (Unit Test) کد مشتری Singleton دشوار باشد زیرا بسیاری از چارچوبهای آزمون به ارثبری برای تولید اشیاء شبیهسازیشده (Mock Objects) متکی هستند.
ارتباط با سایر الگوها
- یک کلاس Facade میتواند بهطور معمول به یک Singleton تبدیل شود، زیرا در بیشتر موارد یک شیء Facade کافی است.
- کارخانههای انتزاعی، سازندهها و نمونهها نیز میتوانند بهعنوان Singleton پیادهسازی شوند.
نتیجهگیری
در این مقاله، الگوی Singleton، موارد کاربرد آن و مزایا و معایب استفاده از این الگوی طراحی را بررسی کردیم. سپس به بررسی پیادهسازیهای مختلف برای الگوی Singleton پرداختیم و ارتباط آن با سایر الگوهای کلاسیک طراحی را نیز بیان کردیم.
الگوی طراحی Singleton در بسیاری از موارد مفید است و اگر بهدرستی استفاده شود، انعطافپذیر است. با این حال، توجه به این نکته ضروری است که الگوی Singleton، مانند سایر الگوهای طراحی ارائهشده توسط گروه Gang of Four، یک راهحل همهجانبه برای طراحی نرمافزار نیست. در نهایت، این مهندسان هستند که باید تصمیم بگیرند چه زمانی از یک الگوی خاص استفاده کنند. در واقع، این الگوها زمانی مفیدند که بهعنوان یک ابزار دقیق به کار گرفته شوند، نه بهعنوان یک ابزار سنگین برای حل تمامی مشکلات.