مقدمه

در درس قبل با ماهیت نمایندگان (Delegates) به عنوان ارجاع‌هایی امن به متدها آشنا شدیم. یکی از قدرتمندترین کاربردهای نمایندگان، ایجاد یک کانال ارتباطی بین اشیاء است که به آن الگوی ناشر-مشترک (Publisher-Subscriber) نیز گفته می‌شود. در این الگو، یک شیء (ناشر) می‌تواند بدون داشتن هیچ اطلاعاتی در مورد اشیاء دیگر (مشترکین)، در مورد تغییر وضعیت خود به آن‌ها "اعلان" (Notify) کند. این کار به ما اجازه می‌دهد تا اجزای مختلف برنامه را از هم جدا (decouple) کرده و کدی بسیار انعطاف‌پذیر و قابل توسعه بنویسیم. در این درس، پیاده‌سازی این الگو را با استفاده از نمایندگان یاد خواهیم گرفت.

مشکل اتصال تنگاتنگ (Tight Coupling)

فرض کنید یک کلاس FileDownloader داریم که وظیفه‌ی دانلود یک فایل را بر عهده دارد. ما می‌خواهیم در حین دانلود، پیشرفت کار را به کاربر نمایش دهیم. یک راه حل ساده‌انگارانه این است که کلاس FileDownloader مستقیماً یک متد از کلاس مسئول نمایش نوار پیشرفت (مثلاً ProgressBar.Update) را فراخوانی کند.

این روش یک مشکل بزرگ در طراحی دارد: FileDownloader حالا به طور مستقیم به کلاس ProgressBar وابسته شده است. این یعنی FileDownloader بیش از حد در مورد جزئیات لایه‌ی نمایش اطلاعات دارد. اگر فردا بخواهیم پیشرفت را به جای نوار پیشرفت، در کنسول یا یک فایل لاگ بنویسیم، باید کد کلاس FileDownloader را تغییر دهیم. این نقض اصل "جداسازی مسئولیت‌ها" و نمونه‌ای از اتصال تنگاتنگ (Tight Coupling) است.

راه حل: استفاده از نمایندگان برای Callback

راه حل صحیح، استفاده از یک نماینده به عنوان مکانیزم Callback است. در این الگو:

  1. کلاس ناشر (FileDownloader) یک نماینده‌ی عمومی تعریف می‌کند. این نماینده، قرارداد یا امضای متدهایی را که می‌توانند به اعلان‌های پیشرفت "گوش دهند"، مشخص می‌کند.
  2. کلاس ناشر یک عضو از نوع آن نماینده را در خود نگه می‌دارد. کلاس‌های دیگر (مشترکین) می‌توانند متدهای خود را به این نماینده "ثبت‌نام" کنند.
  3. در حین انجام کار، کلاس ناشر به سادگی نماینده‌ی خود را فراخوانی می‌کند. با این کار، تمام متدهای ثبت‌نام شده به طور خودکار اجرا می‌شوند، بدون اینکه ناشر بداند آن‌ها چه هستند یا به کدام کلاس تعلق دارند.

پیاده‌سازی کامل الگو

بیایید این الگو را برای مثال FileDownloader پیاده‌سازی کنیم.

Copy Icon FileDownloader.cs
// 1. Define the delegate for progress notifications.
public delegate void ProgressReporter(int percentage);

public class FileDownloader
{
    // 2. Create a public member of the delegate type.
    // Subscribers will register their methods with this delegate.
    public ProgressReporter OnProgress;

    public void DownloadFile()
    {
        Console.WriteLine("Download started...");
        for (int i = 0; i <= 100; i += 10)
        {
            Thread.Sleep(200); // Simulate work

            // 3. If any subscribers exist, invoke the delegate.
            // The '?' checks if OnProgress is not null before calling Invoke.
            OnProgress?.Invoke(i);
        }
        Console.WriteLine("Download finished.");
    }
}

کلاس FileDownloader اکنون کاملاً از نحوه‌ی نمایش پیشرفت جدا شده است. او فقط در زمان مناسب، اعلان خود را ارسال می‌کند.

ثبت‌نام مشترکین

حالا در بخش دیگری از برنامه، می‌توانیم مشترکین مختلفی را برای این اعلان‌ها ثبت‌نام کنیم.

Copy Icon Program.cs
public class Program
{
    public static void Main()
    {
        FileDownloader downloader = new();

        // Create subscribers (methods that match the delegate signature).
        ProgressReporter consoleReporter = WriteProgressToConsole;
        ProgressReporter fileReporter = WriteProgressToFile;

        // Subscribe the methods to the downloader's delegate using '+='.
        downloader.OnProgress += consoleReporter;
        downloader.OnProgress += fileReporter;

        // Start the download.
        downloader.DownloadFile();
    }

    // Subscriber 1: Writes to console.
    public static void WriteProgressToConsole(int percent)
    {
        Console.WriteLine($"Progress: {percent}%");
    }

    // Subscriber 2: Writes to a file.
    public static void WriteProgressToFile(int percent)
    {
        System.IO.File.AppendAllText("progress.log", $"Downloaded {percent}%\n");
    }
}

مزایای الگوی ناشر-مشترک با نمایندگان

  • اتصال سست (Loose Coupling): ناشر (FileDownloader) هیچ اطلاعی از مشترکین خود (WriteProgressToConsole و WriteProgressToFile) ندارد. این اجزا کاملاً از هم مستقل هستند.
  • انعطاف‌پذیری: شما می‌توانید در زمان اجرا، هر تعداد مشترک را به ناشر اضافه یا از آن حذف کنید، بدون اینکه نیاز به تغییر کد ناشر داشته باشید.
  • اساس برنامه‌نویسی رویدادمحور: این الگو دقیقاً همان مکانیزمی است که در پشت صحنه‌ی رویدادها (Events) در C# قرار دارد. رویدادها در واقع یک لایه‌ی امنیتی و ساختاریافته‌تر بر روی نمایندگان چندپخشی هستند که در درس‌های آینده به آن‌ها خواهیم پرداخت.