مقدمه
در درس قبل با ماهیت نمایندگان (Delegates) به عنوان ارجاعهایی امن به متدها آشنا شدیم. یکی از
قدرتمندترین کاربردهای نمایندگان، ایجاد یک کانال ارتباطی بین اشیاء است که به آن الگوی
ناشر-مشترک (Publisher-Subscriber) نیز گفته میشود. در این الگو، یک شیء (ناشر)
میتواند بدون داشتن هیچ اطلاعاتی در مورد اشیاء دیگر (مشترکین)، در مورد تغییر وضعیت خود به آنها
"اعلان" (Notify) کند. این کار به ما اجازه میدهد تا اجزای مختلف برنامه را از هم جدا (decouple)
کرده و کدی بسیار انعطافپذیر و قابل توسعه بنویسیم. در این درس، پیادهسازی این الگو را با استفاده
از نمایندگان یاد خواهیم گرفت.
مشکل اتصال تنگاتنگ (Tight Coupling)
فرض کنید یک کلاس FileDownloader داریم که وظیفهی دانلود یک فایل را بر عهده دارد. ما
میخواهیم
در حین دانلود، پیشرفت کار را به کاربر نمایش دهیم. یک راه حل سادهانگارانه این است که کلاس
FileDownloader مستقیماً یک متد از کلاس مسئول نمایش نوار پیشرفت (مثلاً
ProgressBar.Update) را
فراخوانی کند.
این روش یک مشکل بزرگ در طراحی دارد: FileDownloader حالا به طور مستقیم به کلاس
ProgressBar
وابسته شده است. این یعنی FileDownloader بیش از حد در مورد جزئیات لایهی نمایش اطلاعات
دارد.
اگر فردا بخواهیم پیشرفت را به جای نوار پیشرفت، در کنسول یا یک فایل لاگ بنویسیم، باید کد کلاس
FileDownloader را تغییر دهیم. این نقض اصل "جداسازی مسئولیتها" و نمونهای از
اتصال
تنگاتنگ (Tight Coupling) است.
راه حل: استفاده از نمایندگان برای Callback
راه حل صحیح، استفاده از یک نماینده به عنوان مکانیزم Callback است. در این الگو:
- کلاس ناشر (FileDownloader) یک نمایندهی عمومی تعریف میکند. این نماینده، قرارداد یا
امضای
متدهایی را که میتوانند به اعلانهای پیشرفت "گوش دهند"، مشخص میکند.
- کلاس ناشر یک عضو از نوع آن نماینده را در خود نگه میدارد. کلاسهای دیگر (مشترکین) میتوانند
متدهای خود را به این نماینده "ثبتنام" کنند.
- در حین انجام کار، کلاس ناشر به سادگی نمایندهی خود را فراخوانی میکند. با این کار، تمام
متدهای ثبتنام شده به طور خودکار اجرا میشوند، بدون اینکه ناشر بداند آنها چه هستند یا به
کدام کلاس تعلق دارند.
پیادهسازی کامل الگو
بیایید این الگو را برای مثال FileDownloader پیادهسازی کنیم.
FileDownloader.cs
public delegate void ProgressReporter(int percentage);
public class FileDownloader
{
public ProgressReporter OnProgress;
public void DownloadFile()
{
Console.WriteLine("Download started...");
for (int i = 0; i <= 100; i += 10)
{
Thread.Sleep(200);
OnProgress?.Invoke(i);
}
Console.WriteLine("Download finished.");
}
}
کلاس FileDownloader اکنون کاملاً از نحوهی نمایش پیشرفت جدا شده است. او فقط در زمان
مناسب،
اعلان خود را ارسال میکند.
ثبتنام مشترکین
حالا در بخش دیگری از برنامه، میتوانیم مشترکین مختلفی را برای این اعلانها ثبتنام کنیم.
Program.cs
public class Program
{
public static void Main()
{
FileDownloader downloader = new();
ProgressReporter consoleReporter = WriteProgressToConsole;
ProgressReporter fileReporter = WriteProgressToFile;
downloader.OnProgress += consoleReporter;
downloader.OnProgress += fileReporter;
downloader.DownloadFile();
}
public static void WriteProgressToConsole(int percent)
{
Console.WriteLine($"Progress: {percent}%");
}
public static void WriteProgressToFile(int percent)
{
System.IO.File.AppendAllText("progress.log", $"Downloaded {percent}%\n");
}
}
مزایای الگوی ناشر-مشترک با نمایندگان
- اتصال سست (Loose Coupling): ناشر (FileDownloader) هیچ اطلاعی از
مشترکین
خود (WriteProgressToConsole و WriteProgressToFile) ندارد. این اجزا کاملاً از
هم مستقل
هستند.
- انعطافپذیری: شما میتوانید در زمان اجرا، هر تعداد مشترک را به ناشر اضافه
یا از آن حذف کنید، بدون اینکه نیاز به تغییر کد ناشر داشته باشید.
- اساس برنامهنویسی رویدادمحور: این الگو دقیقاً همان مکانیزمی است که در پشت
صحنهی رویدادها (Events) در C# قرار دارد. رویدادها در واقع یک لایهی امنیتی و
ساختاریافتهتر بر روی نمایندگان چندپخشی هستند که در درسهای آینده به آنها خواهیم پرداخت.