Singleton یک الگوی طراحی سازنده است که این امکان را به ما میدهد تا یک نمونه single از یک آبجکت را ایجاد کنیم و آن نمونه single را بین تمام کاربرانی که به آن نیاز دارند به اشتراک بگذاریم. یک نظر رایجی وجود دارد که گفته میشود الگوی Singleton توصیه نمیشود، زیرا باعث به وجود آمدن code smell (چالش هایی که ممکن است برنامه را با خطا مواجه کند) میشود، اما مواردی هم وجود دارد که استفاده از این الگو در آن کاملا مناسب است.
برای مثال، تعدادی از کامپوننتها دلیلی ندارد که بیشتر از یکبار در پروژه نمونه سازی شوند. برای مثال، یک logger را در نظر بگیرید. ثبت کلاس logger به عنوان یک کامپوننت singleton، کاملا مرسوم است، زیرا تمام کاری که ما باید انجام دهیم این است یک رشته را جهت log شدن در نظر بگیریم و logger آن را برای ما در یک فایل بنویسد. سپس چندین کلاس ممکن است نیاز به نوشتن همزمان در یک فایل از thread های مختلف داشته باشند، بنابراین داشتن یک مکان متمرکز برای این منظور همیشه راه حل خوبی به حساب می آید.
این مقاله، بخشی از مجموعه آموزشی زیر است:
- الگوی طراحی Builder و Fluent Builder
- اینترفیس Fluent Builder به همراه Generic بازگشتی
- Facated Builder
- متد Factory
- Singleton
- Adapter
- Composite
- Decorator
- Command
- Strategy
- Facade
اگر میخواهید در عمل، یک logger را در ASP.NET Core Web API ببینید، میتوانید این مقاله را مطالعه کنید: Log – ASP.NET Core کردن با NLog.
یا شاید گاهی اوقات ما یک task داریم که در آن برخی از داده های یک فایل را میخوانیم و از آنها در پروژه خود استفاده میکنیم. اگر ما از این مسئله اطمینان داشته باشیم که آن فایل در حالیکه داریم آن را میخوانیم، دیگر تغییر نخواهد کرد، میتوانیم یک نمونه single از این آبجکت را ایجاد کنیم که این آبجکت، آن فایل مورد نظر را خوانده و آن را در کل پروژه برای استفاده در کلاسها به اشتراک میگذارد.
در این مقاله، ما قصد داریم به شما نحوه پیاده سازی صحیح الگوی Singleton را در پروژه خود نشان دهیم. منظور ما از پیاده سازی صحیح، این است که کلاس singleton ما، باید thread-safe (یعنی اینکه thread ها، توانایی دسترسی همزمان به یک داده و تغییر آن را نداشته باشند) باشد که در پیاده سازی الگوی Singleton، این یک الزام اساسی میباشد.
سورس را میتوانید از اینجا دانلود کنید: الگوی طراحی Singleton – سورس کد
برای مشاهده لیست کامل مقالات این مجموعه آموزشی، الگوهای طراحی #C را بررسی کنید.
این مقاله، به قسمتهای زیر تقسیم میشود:
آماده سازی پروژه
ما قصد داریم با یک برنامه کنسول ساده، کارمان را شروع کنیم که در آن تمام داده های یک فایل (که شامل شهرها به همراه جمعیت آنها است) را بخوانیم و سپس از آن داده ها استفاده کنیم. بنابراین، برای شروع، یک ایترفیس single ایجاد میکنیم:
1 2 3 4 |
public interface ISingletonContainer { int GetPopulation(string name); } |
بعد از آن، باید یک کلاس برای پیاده سازی اینترفیس ISingletonContainer ایجاد کنیم. نام آن را SingletonDataContainer میگذاریم:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
public class SingletonDataContainer: ISingletonContainer { private Dictionary<string, int> _capitals = new Dictionary<string, int>(); public SingletonDataContainer() { Console.WriteLine("Initializing singleton object"); var elements = File.ReadAllLines("capitals.txt"); for (int i = 0; i < elements.Length; i+=2) { _capitals.Add(elements[i], int.Parse(elements[i + 1])); } } public int GetPopulation(string name) { return _capitals[name]; } } |
بنابراین، ما اینجا، یک dictionary داریم که در آن نام پایتخت کشورها به همراه جمعیت آنها را از فایل خود گرفته و در آن ذخیره می کنیم. همانطور که میبینیم، ما داده ها را درون سازنده خود، از یک فایل میخوانیم. حالا ما آماده ایم در هر جایی، این کلاس را با نمونه سازی از آن استفاده کنیم. اما آیا این واقعا کاری است که ما باید انجام دهیم؟! نمونه سازی از کلاسی که داده ها را از فایلی میخواند که هرگز تغییر نمیکند (در این پروژه بخصوص، جمعیت شهرها، روزانه تغییر میکند). البته که نه. پس بدیهی است که از یک الگوی Singleton اینجا استفاده کنیم که مینواند خیلی کارساز باشد.
پس بریم تا این الگو را پیاده سای کنیم.
پیاده سازی Singleton
برای پیاده سازی الگوی Singleton، کلاس SingletonDataContainer
را تغییر میدهیم:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
public class SingletonDataContainer: ISingletonContainer { private Dictionary<string, int> _capitals = new Dictionary<string, int>(); private SingletonDataContainer() { Console.WriteLine("Initializing singleton object"); var elements = File.ReadAllLines("capitals.txt"); for (int i = 0; i < elements.Length; i+=2) { _capitals.Add(elements[i], int.Parse(elements[i + 1])); } } public int GetPopulation(string name) { return _capitals[name]; } private static SingletonDataContainer instance = new SingletonDataContainer(); public static SingletonDataContainer Instance => instance; } |
بنابراین، کاری که ما در اینجا انجام داده ایم این است که با private کردن سازنده خود، آن را از دید کلاس های مصرف کننده پنهان کردیم. سپس، یک single instance از کلاس خود ایجاد کردیم و آن را از طریق ویژگی Instance، افشاسازی کرده ایم.
در حال حاضر، ما میتوانیم ویژگی Instance را هرچند بار که بخواهیم فراخوانی کنیم، اما آبجکت ما، فقط یکبار نمونه سازی میشود و در هر فراخوانی، به اشتراک گذاشته میشود.
اجازه دهید این تئوری را با هم بررسی کنیم:
1 2 3 4 5 6 7 8 9 10 |
class Program { static void Main(string[] args) { var db = SingletonDataContainer.Instance; var db2 = SingletonDataContainer.Instance; var db3 = SingletonDataContainer.Instance; var db4 = SingletonDataContainer.Instance; } } |
اگر برنامه را اجرا کنیم، این نتیجه را خواهیم دید:
ما اینجا میبینیم که instance خود را چهار بار فراخوانی کردیم، اما آن فقط یکبار initialize شده است که این دقیقا چیزی است که ما میخواهیم. اما پیاده سازی ما ایده آل نیست. اجازه دهید آبجکت خود را به روش lazy بسازیم.
پیاده سازی یک Thread-Safe Singleton
برای پیاده سازی یک thread-safe Singleton با استفاده از نوع Lazy
، کلاس خود را تغییر میدهیم:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
public class SingletonDataContainer : ISingletonContainer { private Dictionary<string, int> _capitals = new Dictionary<string, int>(); private SingletonDataContainer() { Console.WriteLine("Initializing singleton object"); var elements = File.ReadAllLines("capitals.txt"); for (int i = 0; i < elements.Length; i+=2) { _capitals.Add(elements[i], int.Parse(elements[i + 1])); } } public int GetPopulation(string name) { return _capitals[name]; } private static Lazy<SingletonDataContainer> instance = new Lazy<SingletonDataContainer>(() => new SingletonDataContainer()); public static SingletonDataContainer Instance => instance.Value; } |
حالا کلاس ما کاملا thread-safe است. حالا این کلاس به شیوه lazy بارگزاری میشود، به این معنی که instance ما قرار است فقط زمانیکه واقعا مورد نیاز است ایجاد شود. ما حتی اگر بخواهیم میتوانیم با استفاده از ویژگی IsValueCreated
، بررسی کنیم که آیا آبجکت ما ایجاد شده است یا خیر.
بسیار عالی. ما پیاده سازی Singleton خود را به اتمام رساندیم.
حالا به طور کامل، میتوانیم آن را در کلاس مصرف کننده خود، استفاده کنیم:
1 2 3 4 5 6 7 8 9 10 |
class Program { static void Main(string[] args) { var db = SingletonDataContainer.Instance; Console.WriteLine(db.GetPopulation("Washington, D.C.")); var db2 = SingletonDataContainer.Instance; Console.WriteLine(db2.GetPopulation("London")); } } |
بسیار عالی.
نتیجه گیری
ما مشاهده کرده ایم که اگرچه الگوی Singleton چندان مورد استقبال قرار نمی گیرد، اما در برخی موارد می تواند مفید باشد. بنابراین، همیشه به خود ما بستگی دارد که تصمیم بگیریم از آن استفاده کنیم یا نه. نکته اصلی این است که اگر ما نیاز به استفاده از الگوی Singleton در پروژه خود داشته باشیم، این روش خوبی برای انجام آن است.