چکیده: الگوهای طراحی نرمافزار، راهکارهایی استاندارد و آزمودهشده برای حل مسائل رایج در مهندسی نرمافزار به شمار میروند؛ با این حال، استفاده کورکورانه یا افراطی از آنها میتواند به پدیده «مهندسی بیشازحد» (Over-engineering) و تحمیل بدهی فنی منجر شود. در سالهای اخیر، کاربرد کتابخانههای مبتنی بر الگوی واسطگر (Mediator Pattern) مانند MediatR در جامعه توسعهدهندگان داتنت (Microsoft .NET) چنان فراگیر شد که بدون ارزیابی نیاز واقعی، به عنوان پیشفرضِ جداییناپذیر معماری پیازی (Onion Architecture) یا معماری تمیز (Clean Architecture) در نظر گرفته میشد. این مقاله به بررسی آسیبهای ناشی از لایهبندیهای غیرضروری و غیرمستقیمسازی مفرط (Indirection) ناشی از بهکارگیری این الگو پرداخته و نشان میدهد که چگونه قابلیتهای نوین زبان C# و رانتایم مدرن داتنت—نظیر نوعهای رکورد (Records)، نگاشت الگویی (Pattern Matching)، سرویسهای کلیددار (Keyed Services) و تزریق وابستگی صریح—میتوانند بدون وابستگی به کتابخانههای شخص ثالث، معماریهای خواناتر، با کارایی بالاتر و نگهداریپذیرتر را رقم بزنند.
مقدمه
هدف بنیادین الگوهای طراحی و اصول توسعه نرمافزار، مدیریت پیچیدگی و کاهش وابستگیهای متقابل (Decoupling) است. با معرفی و ترویج الگوهایی چون تفکیک مسئولیت فرمان و پرسوجو (CQRS)، تمایل توسعهدهندگان به تفکیک مسیرهای خواندن و نوشتن افزایش یافت. در اکوسیستم داتنت، پیادهسازی این رویکرد معمولاً با بهکارگیری الگوی Mediator و عمدتاً پکیج محبوب MediatR مترادف شد. اگرچه این کتابخانه مزایایی مانند انتزاع رفتارهای فراگیر (Cross-Cutting Concerns) از طریق پایپلاینها را ارائه میدهد، اما ورود بیرویه آن به پروژههای متوسط و حتی سازمانی، چالشهایی بنیادین پدید آورده است. در عمل، لایه Mediator در بسیاری از سیستمها صرفاً به یک «توزیعکننده بوروکراتیک پیام درونحافظهای» تنزل یافته که هیچ ارزش تجاری یا معماری به بار نمیآورد و تنها قابلیت ردیابی جریان کد (Code Traceability) را مختل میکند.
تبیین مساله: هزینههای پنهان کاربرد افراطی واسطگر
۱. توهم جداسازی و تله غیرمستقیمسازی (Indirection Trap)
جداسازی اجزای سیستم (Decoupling) همواره یک فضیلت مطلق نیست؛ بهویژه زمانی که به قیمت پنهانسازی جریان فراخوانی تمام شود. در سناریویی که یک کنترلکننده یا اندپوینت تنها قرار است یک منطق مشخص را اجرا کند، ارسال یک دستور (Command) از طریق واسطگر به یک ادارهکننده (Handler) که ارتباط ۱ به ۱ دارند، مزیت معماری محسوب نمیشود.
این لایهبندی سبب میشود ابزارهای تحلیل ایستای کد و قابلیت «رفتن به پیادهسازی» (Go to Implementation) در محیطهای توسعه (IDE) نتوانند رابطه مستقیم را نمایش دهند و خوانایی کد کاهش یابد.
۲. تخصیصهای مازاد و سربار کارایی (Allocation & Performance Overhead)
اگرچه کتابخانههای نوین داتنت کارآمد هستند، اما استفاده از Mediator معمولاً مستلزم نمونهسازی یک کلاس درخواست، عبور از خطلولههای متعدد، حل مکرر وابستگیها از کانتینر DI در زمان اجرا و تبدیل نوعها (Type Resolution) است.
در سامانههایی با بار ترافیکی بالا (High-Throughput Services)، این انتزاعات غیرضروری موجب تحمیل فشار بر جمعآوری زباله (Garbage Collection) و افت راندمان سیستم میشود.
۳. فراموشی ابزارهای درونی پلتفرم (In-Box Capabilities)
داتنت امروز تفاوت ساختاری عمیقی با داتنت فریمورک سالهای گذشته دارد. تحولات #C و مکانیزمهای توکار پلتفرم، ابزارهای جایگزین و قدرتمندی فراهم کردهاند که نیاز به واسطههای بیرونی را مرتفع میسازند:
- تایپهای رکورد (Records): تعریف ساده و خطی DTOها و مدلهای تغییرناپذیر (Immutable).
- سرویسهای کلیددار (Keyed Services): که در نسخههای اخیر داتنت معرفی شده و امکان حل چندین پیادهسازی از یک رابط را بر اساس کلید بدون نیاز به ساختار Mediator ممکن ساختهاند.
- فیلترهای اندپوینت (Endpoint Filters): در رابطهای برنامهنویسی کمینه (Minimal APIs)، رفتارهای متقاطع نظیر لاگبرداری، اعتبارسنجی و اندازهگیری زمان بهصورت مستقیم روی لایه وب یا لایههای خدمات مدیریت میشوند.
ارزیابی مقایسهای: فراخوانی مستقیم در برابر واسطگر
برای درک تفاوت، به دو سناریوی زیر در ثبت سفارش دقت کنید:
رویکرد غیرمستقیم با استفاده از واسطگر
در این مدل، یک فرمان ساخته شده و بدون مشخص بودن مقصد مستقیم به واسط سپرده میشود:
// کنترلکننده یا اندپوینت
app.MapPost("/orders", async (CreateOrderCommand command, IMediator mediator) =>
{
var orderId = await mediator.Send(command);
return Results.Created($"/orders/{orderId}", new { Id = orderId });
});
// تعریف فرمان و پاسخ
public record CreateOrderCommand(Guid CustomerId, decimal Amount) : IRequest<Guid>;
// ادارهکننده
public class CreateOrderHandler : IRequestHandler<CreateOrderCommand, Guid>
{
public async Task<Guid> Handle(CreateOrderCommand request, CancellationToken ct)
{
// منطق ایجاد سفارش...
return Guid.NewGuid();
}
}در رویکرد بالا، با نگاه کردن به اندپوینت، مشخص نیست چه کلاسی این عملیات را اجرا میکند یا کدام رفتارهای پایپلاین بهصورت پنهان در مسیر دخالت دارند.
رویکرد صریح و مستقیم (Explicit Dependencies)
با حفظ کامل اصول تفکیک وظایف و تستپذیری، میتوان از یک انتزاع مستقیم استفاده کرد:
// قرارداد مستقیم و شفاف
public interface IOrderService
{
ValueTask<Guid> CreateOrderAsync(CreateOrderRequest request, CancellationToken ct = default);
}
public record CreateOrderRequest(Guid CustomerId, decimal Amount);
// اندپوینت خوانا، صریح و بدون سربار
app.MapPost("/orders", async (CreateOrderRequest request, IOrderService orderService, CancellationToken ct) =>
{
var orderId = await orderService.CreateOrderAsync(request, ct);
return Results.Created($"/orders/{orderId}", new { Id = orderId });
});مزایای این روش صریح عبارتند از:
- ردیابی آنی: فشردن کلید ناوبری (F12 در Visual Studio)، توسعهدهنده را مستقیماً به متد مورد نظر یا رابط آن میبرد.
- کاهش حافظه موقت: استفاده از
ValueTask و حذف اشیای میانی، تخصیص حافظه را به حداقل میرساند. - تستپذیری ساده: نوشتن Mock یا Test Double برای
IOrderService بدون نیاز به شبیهسازی کانتینرهای درونی پیچیده انجام میشود.
مدیریت رفتارهای فراگیر بدون وابستگی اضافه
یک استدلال رایج در دفاع از Mediator، پایپلاینهای رفتاری (مانند Logging یا Validation) است. در صورت نیاز به تزریق این رفتارها بر روی خدمات، الگوی تزیینکننده (Decorator) با بهرهگیری از قابلیتهای خود داتنت بهترین جایگزین استاندارد است:
// پیادهسازی اصلی منطق کسبوکار
public class OrderService : IOrderService
{
public ValueTask<Guid> CreateOrderAsync(CreateOrderRequest request, CancellationToken ct = default)
{
// اجرای عملیات تجاری
return ValueTask.FromResult(Guid.NewGuid());
}
}
// تزیینکننده جهت افزودن قابلیت لاگبرداری
public class LoggingOrderServiceDecorator(
IOrderService innerService,
ILogger<LoggingOrderServiceDecorator> logger) : IOrderService
{
public async ValueTask<Guid> CreateOrderAsync(CreateOrderRequest request, CancellationToken ct = default)
{
logger.LogInformation("شروع پردازش سفارش برای مشتری {CustomerId}", request.CustomerId);
var result = await innerService.CreateOrderAsync(request, ct);
logger.LogInformation("سفارش {OrderId} با موفقیت ثبت شد", result);
return result;
}
}این ساختار را میتوان بدون نیاز به هیچ کتابخانه اضافی، مستقیماً در کانتینر پیشفرض داتنت ثبت کرد:
builder.Services.AddScoped<OrderService>();
builder.Services.AddScoped<IOrderService>(sp =>
new LoggingOrderServiceDecorator(
sp.GetRequiredService<OrderService>(),
sp.GetRequiredService<ILogger<LoggingOrderServiceDecorator>>()
));
نتیجهگیری
الگوهای طراحی ابزارهایی برای حل مسائل مشخص هستند، نه احکام ثابتی که باید در هر معماری دیکته شوند. تمایل به پولیشدن، تغییر لایسنس یا بازنگری در کتابخانههای نامآشنا، هشداری سودمند برای جامعه مهندسی نرمافزار است تا وابستگیهای مازاد و پترنهای فرسایشی را ارزیابی مجدد کند.
الگوی واسطگر (Mediator) در سناریوهای خاصی نظیر هماهنگی سیستمهای پیچیده درونفرایندی (Multi-Handler Notifications) یا رویدادهای دامنه ارزش خود را حفظ میکند؛ اما استقرار آن به عنوان شاهکلید تمام تراکنشهای نرمافزاری، پیچیدگی تصادفی (Accidental Complexity) ایجاد میکند. توسعهدهندگان مدرن داتنت با بهرهگیری از ظرفیتهای ذاتی فریمورک و زبان #C میتوانند معماریهایی خلق کنند که صراحت را بر ایهام، و سادگی کارآمد را بر پیچیدگی زینتی ارجح میداند.