الگوی Publish-Subscribe در #C چیست؟ از Delegateها تا میکروسرویسها
در اکوسیستم داتنت (NET.)، الگوی Publish-Subscribe (Pub/Sub) یکی از قدرتمندترین ابزارها برای مدیریت پیچیدگی نرمافزار است. این الگو به ما اجازه میدهد تا اجزای سیستم را با کمترین وابستگی (Loose Coupling) به یکدیگر متصل کنیم.
در C#، این الگو در سه سطح مختلف قابل پیادهسازی است:
-
سطح زبان (Language Level): استفاده از event و delegate.
-
سطح معماری داخلی (In-Process): استفاده از Event Aggregator.
-
سطح سیستم توزیعشده (Distributed): استفاده از Message Brokerها با کتابخانههایی مثل MassTransit.
این مقاله هر سه سطح را با کدهای عملی بررسی میکند.
سطح ۱: مکانیزم بومی C# (Events & Delegates)
زبان C# بر خلاف بسیاری از زبانهای دیگر (مثل جاوا)، پشتیبانی بومی برای Pub/Sub دارد. این کار از طریق کلمات کلیدی delegate و event انجام میشود. این روش برای ارتباط بین کلاسها در یک پروسه واحد عالی است.
سناریو: انکودر ویدیو
فرض کنید کلاسی داریم که ویدیو را فشرده میکند و پس از اتمام کار، باید به سرویس ایمیل و سرویس پیامک اطلاع دهد.
گام اول: تعریف آرگومانهای رویداد
ابتدا دیتایی که قرار است جابجا شود را تعریف میکنیم:
public class VideoEventArgs : EventArgs
{
public string VideoTitle { get; set; }
public DateTime CompletedAt { get; set; }
}
گام دوم: ساخت ناشر (Publisher)
کلاس VideoEncoder نقش ناشر را دارد.
using System;
using System.Threading;
public class VideoEncoder
{
// 1. تعریف Delegate (شکل امضا متد)
// در داتنت مدرن میتوان مستقیماً از EventHandler<T> استفاده کرد
public event EventHandler<VideoEventArgs> VideoEncoded;
public void Encode(string title)
{
Console.WriteLine($"Encoding {title}...");
Thread.Sleep(1000); // شبیهسازی پردازش سنگین
// 3. انتشار رویداد (Publish)
OnVideoEncoded(title);
}
protected virtual void OnVideoEncoded(string title)
{
// بررسی اینکه آیا کسی مشترک شده است یا خیر (Null Check)
VideoEncoded?.Invoke(this, new VideoEventArgs
{
VideoTitle = title,
CompletedAt = DateTime.Now
});
}
}
گام سوم: ساخت مشترکین (Subscribers)
public class MailService
{
// متدی که امضای آن با EventHandler مطابقت دارد
public void OnVideoEncoded(object source, VideoEventArgs e)
{
Console.WriteLine($"MailService: Sending email for {e.VideoTitle}...");
}
}
public class MessageService
{
public void OnVideoEncoded(object source, VideoEventArgs e)
{
Console.WriteLine($"MessageService: Sending SMS for {e.VideoTitle}...");
}
}
گام چهارم: اتصال (Wiring Up)
class Program
{
static void Main(string[] args)
{
var encoder = new VideoEncoder(); // Publisher
var mailService = new MailService(); // Subscriber 1
var messageService = new MessageService(); // Subscriber 2
// اشتراک (Subscription) با استفاده از +=
encoder.VideoEncoded += mailService.OnVideoEncoded;
encoder.VideoEncoded += messageService.OnVideoEncoded;
encoder.Encode("Introduction to C#");
}
}
مزایا: ساده، سریع، تایپسیف (Type-safe).
معایب: ناشر و مشترک باید در یک پروسه باشند و کلاس Program باید به هر دو دسترسی داشته باشد تا آنها را بهم وصل کند.
سطح ۲: الگوی Event Aggregator (برای برنامههای WPF، Xamarin، Blazor)
در برنامههای دسکتاپ یا تکصفحهای پیچیده، ما نمیخواهیم کلاسها یکدیگر را بشناسند یا ارجاع مستقیم (Reference) به هم داشته باشند. در اینجا از یک "هاب مرکزی" به نام Event Aggregator استفاده میکنیم.
پیادهسازی یک Event Aggregator ساده در #C
using System;
using System.Collections.Generic;
using System.Linq;
// اینترفیس پیامی که قرار است منتقل شود
public interface IEvent { }
public class UserLoggedInEvent : IEvent
{
public string Username { get; set; }
}
// هاب مرکزی
public class EventAggregator
{
private readonly Dictionary<Type, List<Action<IEvent>>> _subscribers = new();
// متد اشتراک
public void Subscribe<T>(Action<T> action) where T : IEvent
{
var type = typeof(T);
if (!_subscribers.ContainsKey(type))
{
_subscribers[type] = new List<Action<IEvent>>();
}
// تبدیل Action خاص به Action عمومی برای ذخیرهسازی
_subscribers[type].Add(e => action((T)e));
}
// متد انتشار
public void Publish<T>(T eventToPublish) where T : IEvent
{
var type = typeof(T);
if (_subscribers.ContainsKey(type))
{
foreach (var action in _subscribers[type])
{
action(eventToPublish);
}
}
}
}
نکته: در پروژههای واقعی معمولاً از کتابخانههایی مثل Prism یا MediatR (برای الگوی درخواست/پاسخ و نوتیفیکیشن) استفاده میشود تا مدیریت Threadها و Garbage Collection بهتر انجام شود.
سطح ۳: Pub/Sub توزیعشده با MassTransit و RabbitMQ
وقتی وارد دنیای میکروسرویسها میشویم، C# Events یا Aggregator کار نمیکنند چون سرویسها در سرورهای مختلف اجرا میشوند. در اینجا به یک Message Broker (مثل RabbitMQ) نیاز داریم.
در دنیای .NET، استاندارد دفاکتو برای ارتباط با بروکرها، کتابخانه MassTransit است. این کتابخانه پیچیدگیهای RabbitMQ را مخفی کرده و یک API تمیز C# ارائه میدهد.
سناریو: سیستم ثبت سفارش (Order Processing)
۱. تعریف قرارداد پیام (Shared Contract)
این کلاس باید بین پروژه ناشر و مشترک مشترک باشد (مثلاً در یک Class Library جداگانه).
public interface OrderCreated
{
Guid OrderId { get; }
decimal Amount { get; }
}
۲. پیکربندی مشترک (Consumer) در ASP.NET Core
ابتدا پکیج MassTransit.RabbitMQ را نصب کنید.
// کلاسی که پیام را دریافت و پردازش میکند
public class OrderCreatedConsumer : IConsumer<OrderCreated>
{
public async Task Consume(ConsumeContext<OrderCreated> context)
{
var message = context.Message;
Console.WriteLine($"سفارش جدید دریافت شد: {message.OrderId} با مبلغ {message.Amount}");
// انجام کارهای ناهمگام (Database, Email, etc)
await Task.Delay(100);
}
}
پیکربندی در Program.cs:
builder.Services.AddMassTransit(x =>
{
x.AddConsumer<OrderCreatedConsumer>();
x.UsingRabbitMq((context, cfg) =>
{
cfg.Host("localhost", "/", h => {
h.Username("guest");
h.Password("guest");
});
cfg.ReceiveEndpoint("order-service-queue", e =>
{
e.ConfigureConsumer<OrderCreatedConsumer>(context);
});
});
});
۳. انتشار پیام (Publisher)
در کنترلر API یا سرویس دیگر، اینترفیس IPublishEndpoint را تزریق (Inject) میکنیم.
[ApiController]
[Route("api/[controller]")]
public class OrdersController : ControllerBase
{
private readonly IPublishEndpoint _publishEndpoint;
public OrdersController(IPublishEndpoint publishEndpoint)
{
_publishEndpoint = publishEndpoint;
}
[HttpPost]
public async Task<IActionResult> CreateOrder(CreateOrderDto dto)
{
// منطق ذخیره در دیتابیس...
// انتشار پیام به صورت همگانی (Broadcast)
await _publishEndpoint.Publish<OrderCreated>(new
{
OrderId = Guid.NewGuid(),
Amount = dto.Amount
});
return Ok("سفارش ثبت شد و رویداد منتشر گردید.");
}
}
در این روش، MassTransit به صورت خودکار یک Exchange در RabbitMQ میسازد و پیام را به تمام صفهایی که به این رویداد گوش میدهند (Subscribe کردهاند) تحویل میدهد.

روش مدرن: C# Channels (Producer/Consumer در حافظه)
از نسخه .NET Core 3.0 به بعد، فضای نام System.Threading.Channels اضافه شد که برای پیادهسازی Pub/Sub با کارایی بسیار بالا (High Performance) در داخل یک برنامه استفاده میشود. این روش Thread-Safe است و از async/await پشتیبانی میکند.
using System.Threading.Channels;
var channel = Channel.CreateUnbounded<string>();
// Producer (Publisher)
_ = Task.Run(async () =>
{
for (int i = 0; i < 5; i++)
{
await channel.Writer.WriteAsync($"Message {i}");
await Task.Delay(500);
}
channel.Writer.Complete(); // پایان ارسال
});
// Consumer (Subscriber)
await foreach (var item in channel.Reader.ReadAllAsync())
{
Console.WriteLine($"Received from channel: {item}");
}
مقایسه روشهای Pub/Sub در C#
|
روش |
ابزار/تکنولوژی |
کاربرد |
پیچیدگی |
|
Events/Delegates |
C# Native |
ارتباط بین کلاسهای UI و Logic |
کم |
|
Event Aggregator |
MediatR / Prism |
ارتباط بین ماژولهای جداگانه در یک برنامه (Decoupling) |
متوسط |
|
Channels |
System.Threading.Channels |
پردازشهای پسزمینه (Background Tasks) سریع و امن |
متوسط |
|
Distributed |
MassTransit / RabbitMQ / Azure Service Bus |
میکروسرویسها و سیستمهای توزیعشده |
زیاد |
نکات کلیدی برای توسعهدهندگان #C
-
مدیریت Memory Leaks: در استفاده از event های بومی C#، اگر مشترک (Subscriber) طول عمر کمتری نسبت به ناشر (Publisher) داشته باشد و اشتراک خود را لغو نکند (-=)، Garbage Collector نمیتواند آن را از حافظه پاک کند. این یکی از دلایل اصلی Memory Leak در داتنت است.
-
استفاده از Async/Await: در سیستمهای مدرن، هندل کردن رویدادها اغلب شامل I/O (مثل دیتابیس یا شبکه) است. تا حد امکان از ساختارهایی استفاده کنید که از Task پشتیبانی میکنند (مثل MassTransit یا MediatR) و از async void در رویدادهای بومی پرهیز کنید (مگر در UI Handlers).
-
تزریق وابستگی (DI): در داتنت مدرن، همیشه سعی کنید Event Aggregator یا Message Bus را از طریق DI Container (IServiceCollection) به کلاسها تزریق کنید تا تستپذیری (Unit Testing) حفظ شود.
جمعبندی
الگوی Pub/Sub در C# تنها یک تکنیک نیست، بلکه طیفی از راهکارهاست.1
-
برای یک فرم ساده ویندوزی؟ از Events استفاده کنید.2
-
برای یک برنامه ماژولار بزرگ؟ از MediatR استفاده کنید.3
-
برای پردازش دادههای سریع؟ از Channels استفاده کنید.4
-
برای اتصال چندین میکروسرویس؟ از MassTransit اس5تفاده کنید.
انتخاب ابزار مناسب، هنر معمار نرمافزار است.
0 نظر
هنوز نظری برای این مقاله ثبت نشده است.