مقدمه
الگوی Mediator (واسطه) یکی از الگوهای طراحی رفتاری است که با هدف کاهش وابستگی مستقیم بین اشیاء و سادهسازی ارتباطات میان آنها در برنامهنویسی استفاده میشود. در این الگو، یک شیء مرکزی به نام Mediator مسئولیت هماهنگی و مدیریت تعاملات بین اجزای مختلف را بر عهده دارد. در اکوسیستم داتنت (Microsoft .NET Framework)، این الگو بهویژه در پروژههایی که از معماریهایی مانند CQRS (جداسازی مسئولیت دستورات و پرسوجوها) استفاده میکنند، جایگاه ویژهای دارد. دو ابزار متنباز برجسته که پیادهسازی این الگو را تسهیل میکنند، عبارتاند از پروژههایی که در GitHub با عناوین
MediatR و
Mediator شناخته میشوند. این مقاله با هدف معرفی این ابزارها، بررسی ویژگیهای کلیدی، نقاط مشترک و تفاوتها، ارائه مثالهای عملی و مقایسه آنها تدوین شده است تا توسعهدهندگان داتنت بتوانند انتخاب آگاهانهای برای پروژههای خود داشته باشند.
معرفی ابزارها
MediatR یک کتابخانه ساده و بدون پیچیدگی اضافی است که برای پیادهسازی الگوی Mediator در داتنت طراحی شده است. این ابزار با تمرکز بر ارسال پیامها بهصورت درونفرآیندی (In-Process Messaging)، امکان جداسازی منطق برنامه از لایههای مختلف مانند لایه ارائه (Presentation Layer) را فراهم میکند. این کتابخانه از درخواستها (Requests)، اعلانها (Notifications) و رفتارهای خط لوله (Pipeline Behaviors) پشتیبانی میکند و بهطور گسترده در پروژههای داتنت مورد استفاده قرار میگیرد.
Mediator اما رویکرد متفاوتی دارد و با بهرهگیری از قابلیت Source Generators در داتنت 5 به بعد، یک پیادهسازی با کارایی بالا ارائه میدهد. این ابزار با الهام از MediatR، برخی محدودیتهای آن را برطرف کرده و بر بهینهسازی عملکرد و کاهش تخصیص حافظه تمرکز دارد. هر دو ابزار در نهایت هدف مشترکی دارند: سادهسازی ارتباطات و کاهش وابستگیها؛ اما در روش پیادهسازی و ویژگیها تفاوتهایی دارند که در ادامه بررسی میشوند.
ویژگیهای کلیدی
MediatR
- سادگی و انعطافپذیری: MediatR با رابط کاربری ساده، از پیامرسانی درخواست/پاسخ (Request/Response) و اعلانها (Notifications) پشتیبانی میکند. این ابزار به توسعهدهندگان اجازه میدهد تا بهراحتی دستورات (Commands) و پرسوجوها (Queries) را مدیریت کنند.
- پشتیبانی از خط لوله: امکان افزودن رفتارهای خط لوله مانند اعتبارسنجی (Validation) یا لاگگیری (Logging) وجود دارد.
- وابستگی به DI: این ابزار بهطور کامل با Microsoft.Extensions.DependencyInjection یکپارچه است و ثبت سرویسها را ساده میکند.
- عملکرد مبتنی بر بازتاب: MediatR از بازتاب (Reflection) در زمان اجرا برای یافتن Handlerها استفاده میکند که ممکن است در پروژههای بزرگتر تأثیر منفی بر عملکرد داشته باشد.
Mediator
- بهینهسازی با Source Generators: برخلاف MediatR، این ابزار از تولید کد در زمان کامپایل استفاده میکند که نیاز به Reflection را حذف کرده و عملکرد را بهبود میبخشد.
- کاهش تخصیص حافظه: با استفاده از ValueTask بهجای Task و بهینهسازیهایی مانند PoolingAsyncValueTaskMethodBuilder، مصرف حافظه کاهش مییابد.
- تشخیص خطا در زمان کامپایل: اگر Handler برای یک درخواست تعریف نشده باشد، اخطاری در زمان کامپایل تولید میشود که خطاها را زودتر آشکار میکند.
- پشتیبانی از جریانهای ناهمگام: قابلیتهایی مانند IAsyncEnumerable برای مدیریت جریانهای داده اضافه شده است.
نقاط مشترک
هر دو ابزار در موارد زیر اشتراک دارند:
- پشتیبانی از الگوی Mediator: هر دو بهعنوان واسطهای برای مدیریت درخواستها و اعلانها عمل میکنند و وابستگی مستقیم بین اجزا را کاهش میدهند.
- سازگاری با DI: هر دو با سیستم تزریق وابستگی (Dependency Injection) داتنت سازگارند و از Microsoft.Extensions.DependencyInjection پشتیبانی میکنند.
- تمرکز بر CQRS: هر دو ابزار بهطور خاص برای پیادهسازی الگوی CQRS طراحی شدهاند و جداسازی دستورات و پرسوجوها را تسهیل میکنند.
- مدیریت نگرانیهای متقاطع: امکان افزودن منطقهایی مانند لاگگیری، اعتبارسنجی یا کشینگ (Caching) از طریق خط لوله وجود دارد.
تفاوتها و نقاط قوت
- عملکرد: Mediator با استفاده از Source Generators عملکرد بهتری نسبت به MediatR دارد، زیرا از Reflection اجتناب میکند. این مزیت در پروژههای بزرگ با تعداد پیامهای زیاد (بیش از 500 پیام) مشهودتر است.
- پیچیدگی استفاده: MediatR به دلیل سادگی و مستندات گسترده، برای تازهکاران مناسبتر است، در حالی که Mediator به دانش بیشتری از Source Generators نیاز دارد.
- انعطافپذیری: MediatR از درخواستهای عمومی (Generic Requests) پشتیبانی میکند، اما Mediator این قابلیت را ندارد که ممکن است در برخی سناریوها محدودیت ایجاد کند.
- پشتیبانی از جریانها: Mediator با ارائه IAsyncEnumerable قابلیتهای پیشرفتهتری برای سناریوهای مبتنی بر جریان دارد، در حالی که MediatR این ویژگی را بهصورت پیشفرض ارائه نمیدهد.
مثالهای عملی پیادهسازی
مثال با MediatR
در این مثال، یک درخواست ساده برای دریافت پاسخ از یک Handler تعریف میکنیم:
public class Ping : IRequest<string> { }
public class PingHandler : IRequestHandler<Ping, string>
{
public Task<string> Handle(Ping request, CancellationToken cancellationToken)
{
return Task.FromResult("Pong");
}
}
// استفاده در برنامه
var mediator = serviceProvider.GetRequiredService<IMediator>();
var response = await mediator.Send(new Ping());
Console.WriteLine(response); // خروجی: Pongدر اینجا، MediatR درخواست را به Handler مربوطه ارسال میکند و پاسخ را برمیگرداند.
مثال با Mediator
در این مثال، یک درخواست جریاندار (Streaming) پیادهسازی میکنیم:
public sealed record StreamPing(Guid Id) : IStreamRequest<Pong>;
public sealed record Pong(Guid Id);
public sealed class PingHandler : IStreamRequestHandler<StreamPing, Pong>
{
public async IAsyncEnumerable<Pong> Handle(StreamPing request, [EnumeratorCancellation] CancellationToken cancellationToken)
{
for (int i = 0; i < 5; i++)
{
await Task.Delay(1000, cancellationToken);
yield return new Pong(request.Id);
}
}
}
// استفاده در برنامه
var mediator = serviceProvider.GetRequiredService<IMediator>();
var ping = new StreamPing(Guid.NewGuid());
await foreach (var pong in mediator.CreateStream(ping))
{
Console.WriteLine("Received pong!");
}این کد یک جریان ناهمگام را نشان میدهد که Mediator به لطف Source Generators و IAsyncEnumerable بهینهتر مدیریت میکند.
نتیجهگیری
الگوی Mediator ابزاری قدرتمند برای مدیریت ارتباطات و کاهش وابستگیها در برنامههای داتنت است. دو کتابخانه MediatR و Mediator هر کدام با ویژگیها و نقاط قوت خاص خود، این الگو را پیادهسازی میکنند. MediatR با سادگی و انعطافپذیری، گزینهای عالی برای پروژههای کوچک تا متوسط است، در حالی که Mediator با تمرکز بر عملکرد و بهینهسازی، برای پروژههای پیچیده و حساس به کارایی مناسبتر است. انتخاب بین این دو به نیازهای پروژه، مقیاس آن و سطح دانش تیم بستگی دارد.