الگوی طراحی رفتاری میانجی (Mediator) به ما کمک میکند تا وابستگیهای بینظم بین اشیا را کاهش دهیم. این الگو ارتباط مستقیم بین موجودیتها را محدود کرده و آنها را مجبور میکند که تنها از طریق یک شیء میانجی با یکدیگر همکاری کنند.
میتوانید کد نمونه این پست را در GitHub پیدا کنید.
مفهومسازی مسئله
فرض کنید ما یک دیالوگ رابط کاربری برای ایجاد و ویرایش یک پروفایل مشتری داریم. این دیالوگ شامل اجزای مختلفی مثل فیلدهای متنی، چکباکسها، دکمهها، تبها و غیره است.
برخی از این عناصر ممکن است با یکدیگر تعامل داشته باشند. بهعنوان مثال، انتخاب چکباکس «من هماتاقی دارم» ممکن است یک فیلد متنی مخفی را برای وارد کردن نام هماتاقی نمایش دهد. یا مثلاً، دکمه ارسال ممکن است وظیفه اعتبارسنجی مقادیر تمام فیلدها را قبل از ارسال دادهها به عهده داشته باشد.
اگر این منطق به صورت مستقیم توسط عناصر فرم پیادهسازی شود، استفاده مجدد از این عناصر در بخشهای دیگر برنامه دشوار خواهد شد. برای مثال، نمیتوانیم از همان چکباکس در فرم دیگری استفاده کنیم، زیرا این چکباکس به فیلد متنی هماتاقی وابسته است. در چنین شرایطی، مجبوریم یا تمام کلاسهای مربوط به فرم پروفایل را استفاده کنیم یا هیچکدام را.
الگوی طراحی میانجی پیشنهاد میکند که تمام ارتباطات مستقیم بین اجزایی که باید مستقل از یکدیگر باشند را متوقف کنیم. در عوض، این اجزا باید به صورت غیرمستقیم و از طریق یک شیء میانجی خاص ارتباط برقرار کنند. این شیء میانجی تماسها را به اجزای مناسب هدایت میکند. در نتیجه، اجزا تنها به یک کلاس میانجی وابسته هستند و دیگر به یکدیگر وابسته نیستند.
در مثال ما، کلاس دیالوگ میتواند نقش میانجی را ایفا کند. از آنجایی که کلاس دیالوگ احتمالاً از قبل از تمام زیرعناصر خود آگاه است، حتی نیازی به افزودن وابستگیهای جدید به این کلاس نخواهیم داشت.
بزرگترین تغییر در عناصر واقعی فرم اتفاق میافتد. به عنوان مثال، دکمه ارسال را در نظر بگیرید. در پیادهسازی قبلی، هر بار که کاربر روی دکمه کلیک میکرد، باید مقادیر تمام عناصر فرم را اعتبارسنجی میکرد. اکنون تنها کاری که باید انجام دهد این است که دیالوگ را از کلیک آگاه کند. پس از دریافت این اعلان، خود دیالوگ اعتبارسنجی را انجام میدهد یا این کار را به اجزای جداگانه واگذار میکند. بنابراین، به جای وابستگی به دهها عنصر، دکمه فقط به کلاس دیالوگ وابسته است.
میتوانیم این وابستگی را حتی بیشتر کاهش دهیم، با استخراج یک رابط عمومی برای همه نوع دیالوگها. این رابط یک متد اعلان تعریف میکند که همه عناصر فرم میتوانند برای اطلاعرسانی رویدادهای خود به دیالوگ استفاده کنند. بنابراین، دکمه ارسال ما اکنون باید بتواند با هر دیالوگی که آن رابط را پیادهسازی میکند کار کند.
به این ترتیب، الگوی میانجی به ما اجازه میدهد تا یک شبکه پیچیده از روابط بین اشیا را در یک شیء میانجی واحد کپسوله کنیم. هرچه وابستگیهای یک کلاس کمتر باشد، تغییر، گسترش یا استفاده مجدد از آن آسانتر میشود.
ساختار الگوی طراحی میانجی
در پیادهسازی پایه، الگوی میانجی دارای سه شرکتکننده است:
کامپوننت (Component):
کامپوننتها کلاسهای مختلفی هستند که شامل منطق تجاری خاصی هستند. هر کامپوننت یک ارجاع به میانجی دارد که از طریق رابط میانجی اعلام شده است. کامپوننت از کلاس واقعی میانجی آگاه نیست، بنابراین میتوانیم با اتصال آن به میانجی دیگری، از کامپوننت مجدداً استفاده کنیم. کامپوننتها نباید از وجود دیگر کامپوننتها مطلع باشند. اگر اتفاق مهمی در یک کامپوننت رخ دهد، باید میانجی را مطلع کند. زمانی که میانجی اعلان را دریافت میکند، میتواند فرستنده را شناسایی کرده و تصمیم بگیرد کدام کامپوننت باید فعال شود.میانجی (Mediator):
رابط میانجی متدهایی برای ارتباط با کامپوننتها اعلام میکند که اغلب شامل یک متد اعلان میشود. کامپوننتها میتوانند هر اطلاعاتی را بهعنوان آرگومانهای این متد ارسال کنند، اما فقط به گونهای که هیچ گونه وابستگی ایجاد نشود.میانجی خاص (Concrete Mediator):
میانجیهای خاص روابط بین کامپوننتهای مختلف را کپسوله میکنند. میانجیهای خاص اغلب ارجاعات به تمام کامپوننتهایی که مدیریت میکنند را نگه میدارند و گاهی حتی چرخه حیات آنها را مدیریت میکنند.
برای نشان دادن نحوه کار الگوی میانجی، سیستمی را مدلسازی میکنیم که در آن غرفههای خوراکی در پارکهای تفریحی بزرگ با یکدیگر ارتباط دارند.
پارکهای تفریحی معمولاً یک فضای مرکزی برای فروش غذا دارند که شامل غرفههای خوراکی و چندین واحد کوچکتر است که مشتریان میتوانند غذاها و نوشیدنیهای خود را سفارش دهند.
اما فروش غذا به مشتریان گرسنه نیازمند تأمین منابع است و گاهی ممکن است غرفههای مختلف از برخی اقلام مورد نیاز خود بینصیب بمانند. بیایید سیستمی را تصور کنیم که در آن غرفههای مختلف میتوانند با یکدیگر صحبت کنند و اعلام کنند که به چه منابعی نیاز دارند و کدام غرفه این منابع را دارد. ما میتوانیم این سیستم را با استفاده از الگوی میانجی مدلسازی کنیم.
ابتدا به یک رابط میانجی نیاز داریم که متدی برای ارتباط بین غرفهها تعریف کند:
using Mediator.Components;
namespace Mediator
{
///
/// The Mediator interface, which defines a send message
/// method which the concrete mediators must implement.
///
public interface IMediator
{
public void SendMessage(string message, SnackBar snackBar);
}
}
ما همچنین به یک کلاس انتزاعی (Abstract Class) نیاز داریم تا اجزایی را که با یکدیگر تعامل دارند، نمایندگی کند:
namespace Mediator.Components
{
///
/// The SnackBar abstract class represents an
/// entity involved in the conversation which
/// should receive messages.
///
public class SnackBar
{
protected IMediator _mediator;
public SnackBar(IMediator mediator)
{
_mediator = mediator;
}
}
}
حال بیایید اجزای مختلف را پیادهسازی کنیم. در این مثال، دو اسنک بار داریم: یکی که هاتداگ میفروشد و دیگری سیبزمینی سرخکرده.
namespace Mediator.Components
{
///
/// A Concrete Component class
///
public class HotDogStand : SnackBar
{
public HotDogStand(IMediator mediator) : base(mediator)
{
}
public void Send(string message)
{
Console.WriteLine($"HotDog Stand says: {message}");
_mediator.SendMessage(message, this);
}
public void Notify(string message)
{
Console.WriteLine($"HotDog Stand gets message: {message}");
}
}
}
namespace Mediator.Components
{
///
/// A Concrete Component class
///
public class FrenchFriesStand : SnackBar
{
public FrenchFriesStand(IMediator mediator) : base(mediator)
{
}
public void Send(string message)
{
Console.WriteLine($"French Fries Stand says: {message}");
_mediator.SendMessage(message, this);
}
public void Notify(string message)
{
Console.WriteLine($"French Fries Stand gets message: {message}");
}
}
}
توجه داشته باشید که هر کامپوننت باید از میانجیای که پیامهای غرفه خوراکی را مدیریت میکند آگاه باشد.
در نهایت، میتوانیم کلاس ConcreteMediator را پیادهسازی کنیم. این کلاس یک ارجاع به هر کامپوننت نگه میدارد و ارتباطات بین آنها را مدیریت میکند.
using Mediator.Components;
namespace Mediator
{
///
/// The Concrete Mediator class, which implements the send message
/// method and keep track of all participants in the conversation.
///
public class SnackBarMediator : IMediator
{
private HotDogStand hotDogStand;
private FrenchFriesStand friesStand;
public HotDogStand HotDogStand { set { hotDogStand = value; } }
public FrenchFriesStand FriesStand { set { friesStand = value; } }
public void SendMessage(string message, SnackBar snackBar)
{
if (snackBar == hotDogStand)
friesStand.Notify(message);
if (snackBar == friesStand)
hotDogStand.Notify(message);
}
}
}
در متد Main()
، میتوانیم از میانجی خود برای شبیهسازی مکالمه بین دو غرفه خوراکی استفاده کنیم. فرض کنید یکی از غرفهها روغن پختوپز تمام کرده است و میخواهد بداند که آیا غرفه دیگر روغن اضافی دارد که از آن استفاده نمیکند:
using Mediator;
using Mediator.Components;
SnackBarMediator mediator = new SnackBarMediator();
HotDogStand leftKitchen = new HotDogStand(mediator);
FrenchFriesStand rightKitchen = new FrenchFriesStand(mediator);
mediator.HotDogStand = leftKitchen;
mediator.FriesStand = rightKitchen;
leftKitchen.Send("Can you send more cooking oil?");
rightKitchen.Send("Sure thing, Homer's on his way");
rightKitchen.Send("Do you have any extra soda? We've had a rush on them over here.");
leftKitchen.Send("Just a couple, we'll send Homer back with them");
Console.ReadKey();
اگر این برنامه را اجرا کنیم، گفتوگویی بین دو غرفه خوراکی نمایش داده میشود.
کتابخانه MediatR
کتابخانه MediatR خود را بهعنوان “پیادهسازی ساده میانجی در .NET” توصیف میکند. MediatR اساساً کتابخانهای است که پیامرسانی درونفرآیندی را تسهیل میکند.
نصب MediatR
ابتدا باید بسته MediatR NuGet را نصب کنیم. برای این کار، از کنسول مدیریت بسته میتوانیم دستور زیر را اجرا کنیم:
Install-Package MediatR
همچنین به یک بسته افزایشی نیاز داریم که ما را قادر به استفاده از تزریق وابستگی میکند.
Install-Package MediatR.Extensions.Microsoft.DependencyInjection
حالا میتوانیم وابستگیهای MeiatR را راهاندازی کنیم:
private static IMediator BuildMediator(WrappingWriter writer)
{
var services = new ServiceCollection();
services.AddSingleton(writer);
services.AddMediatR(typeof(Ping));
var provider = services.BuildServiceProvider();
return provider.GetRequiredService();
}
ایجاد هندلرها
MediatR دارای دو نوع پیام است: ارسال و دریافت و پخش گسترده (برادکست). در این مثال یک سیستم ساده بررسی سلامت را شبیهسازی خواهیم کرد که در آن کلاینت به سرور پینگ میفرستد و در صورتی که سرور سالم باشد جواب آن را با پانگ ارسال میکند.
ابتدا یک کلاس ساده Ping
پیادهسازی خواهیم کرد که رابط IRequest
را پیادهسازی میکند.
namespace MediatRExample
{
public class Ping : IRequest
{
public string Message { get; set; }
}
}
به یک هندلر پیام نیاز داریم. به این منظور تنها باید رابط IRequestHandler
را پیادهسازی کنیم.
namespace MediatRExample
{
public class PingHandler : IRequestHandler
{
public Task Handle(Ping request, CancellationToken cancellationToken)
{
return Task.FromResult("Pong");
}
}
}
استفاده از سرویس میانجی
در نهایت، ما یک کلاس اجراکننده خواهیم ساخت که سرویس میانجی ما را فراخوانی میکند. این متد ابتدا با استفاده از نمونه writer، متن “Sending Ping…” را به جریان خروجی مینویسد. سپس، متد Send از نمونه mediator را فراخوانی کرده و یک نمونه جدید از کلاس Ping با پیام “Ping” را به آن ارسال میکند. متد Send پیام Ping را به Handler مربوطه که باید آن را پردازش کند ارسال کرده و سپس پاسخ را برمیگرداند:
using MediatR;
namespace MediatRExample
{
public static class Runner
{
public static async Task Run(IMediator mediator, WrappingWriter writer)
{
await writer.WriteLineAsync("Sending Ping...");
var pong = await mediator.Send(new Ping { Message = "Ping" });
await writer.WriteLineAsync("Received: " + pong);
await writer.WriteLineAsync();
}
}
}
در نهایت، ما به متد اصلی نیاز داریم تا همه چیز را اجرا کند.
متد BuildMediator
یک شیء جدید از نوع ServiceCollection
ایجاد کرده و اشیاء TextWriter
و کتابخانه MediatR
را به آن اضافه میکند. سپس، یک نمونه جدید از نوع ServiceProvider
ساخته و نمونه IMediator
را از ارائهدهنده دریافت میکند.
در ابتدای متد Main
، یک نمونه جدید از کلاس WrappingWriter
ایجاد میشود. این کلاس جریان Console.Out
را بستهبندی میکند. سپس، متد BuildMediator
فراخوانی شده و نمونه writer
به عنوان پارامتر به آن داده میشود تا یک نمونه جدید از IMediator
ساخته شود.
در نهایت، متد Main
متد Runner.Run
را فراخوانی کرده و نمونههای IMediator
و writer را به آن میدهد تا پیام Ping
را از طریق میانجیگر اجرا کرده و نتیجه را به جریان خروجی بنویسد.
using MediatR;
namespace MediatRExample
{
public static class Program
{
public static Task Main(string[] args)
{
var writer = new WrappingWriter(Console.Out);
var mediator = BuildMediator(writer);
return Runner.Run(mediator, writer);
}
private static IMediator BuildMediator(WrappingWriter writer)
{
var services = new ServiceCollection();
services.AddSingleton(writer);
services.AddMediatR(typeof(Ping));
var provider = services.BuildServiceProvider();
return provider.GetRequiredService();
}
}
}
تفاوت بین الگوی طراحی میانجی (Mediator) و مشاهدهگر (Observer)
تفاوت بین الگوی میانجی و مشاهدهگر اغلب گمراهکننده است. در بیشتر موارد، میتوانیم از هر یک از این الگوها استفاده کنیم، اما مواردی نیز وجود دارد که میتوان هر دو را به کار برد.
هدف اصلی الگوی میانجی این است که وابستگیها بین مجموعهای از اجزا را حذف کند. این اجزا به جای وابستگی به یکدیگر، به یک شیء میانجی وابسته میشوند. از سوی دیگر، هدف اصلی الگوی مشاهدهگر ایجاد ارتباطات یکطرفه و پویا بین اشیا است، به طوری که برخی از اشیا نقش تابع یا زیرمجموعهی دیگران را ایفا میکنند.
یکی از پیادهسازیهای محبوب الگوی میانجی بر اساس الگوی مشاهدهگر انجام میشود. در این پیادهسازی، میانجی نقش منتشرکننده را ایفا میکند و اجزا به عنوان مشترکین رویدادهای میانجی عمل میکنند. هنگامی که الگوی میانجی به این صورت پیادهسازی شود، بسیار شبیه به الگوی مشاهدهگر به نظر میرسد.
اگر در تشخیص اینکه آیا از الگوی میانجی استفاده کردهاید یا مشاهدهگر، دچار سردرگمی شدید، به یاد داشته باشید که الگوی میانجی میتواند به روشهای دیگری نیز پیادهسازی شود. به عنوان مثال، میتوانید همه اجزا را به طور دائمی به یک شیء میانجی متصل کنید. این پیادهسازی شبیه الگوی مشاهدهگر نخواهد بود، اما همچنان نمونهای از الگوی میانجی است.
اکنون اگر برنامهای دارید که در آن همه اجزا به عنوان منتشرکننده عمل میکنند و ارتباطات پویا بین یکدیگر دارند، در واقع یک مجموعه توزیعشده از مشاهدهگرها دارید، زیرا هیچ شیء میانجی متمرکزی وجود ندارد.
مزایا و معایب الگوی طراحی میانجی
مزایا
✔ سادگی درک و نگهداری: ارتباطات بین اجزا در یک مکان متمرکز میشود که این امر باعث سادهتر شدن درک و نگهداری آن میشود و اصل مسئولیتپذیری یگانه را برآورده میسازد.
- قابلیت معرفی میانجیهای جدید: بدون نیاز به تغییر در اجزای واقعی میتوان میانجیهای جدید معرفی کرد.
- کاهش وابستگی بین اجزا: وابستگی میان اجزا کاهش مییابد.
- امکان استفاده مجدد از اجزا: استفاده مجدد از اجزای مستقل آسانتر میشود.
معایب
- تبدیل شدن به شیء خدا: میانجی میتواند با گذشت زمان به یک شیء بسیار بزرگ و پیچیده تبدیل شود.
- ایجاد نقطه شکست واحد: ممکن است به دلیل وجود تنها یک میانجی، نقطهای از شکست ایجاد شود. همچنین ممکن است به دلیل ارتباط غیرمستقیم بین ماژولها، افت عملکرد رخ دهد.
رابطه با سایر الگوها
- زنجیره مسئولیت (Chain of Responsibility)، فرمان (Command)، میانجی (Mediator) و مشاهدهگر (Observer): این الگوها روشهای مختلفی برای اتصال ارسالکنندگان و گیرندگان درخواستها ارائه میدهند:
- الگوی زنجیره مسئولیت: درخواست را به صورت متوالی در طول یک زنجیره پویا از گیرندگان ارسال میکند تا یکی از آنها آن را پردازش کند.
- الگوی فرمان: کانالهای ارتباطی یکطرفه بین ارسالکنندگان و گیرندگان ایجاد میکند.
- الگوی میانجی: ارتباط مستقیم بین ارسالکنندگان و گیرندگان را حذف کرده و آنها را مجبور میکند از طریق یک شیء میانجی ارتباط برقرار کنند.
- الگوی مشاهدهگر: به گیرندگان اجازه میدهد به صورت پویا در دریافت درخواستها ثبتنام یا لغو ثبتنام کنند.
نتیجهگیری
در این مقاله، الگوی میانجی، زمان استفاده از آن و مزایا و معایب آن را بررسی کردیم. همچنین به کتابخانه معروف MediatR و ارتباط میان الگوی میانجی با سایر الگوهای طراحی کلاسیک پرداختیم.
الگوی طراحی میانجی وابستگیها را به شدت کاهش میدهد و تعامل میان اشیا را در برنامههای نرمافزاری شما بهبود میبخشد. این الگو ابزاری مفید برای حل بسیاری از مشکلات رایج در طراحی نرمافزار است. الگوی میانجی میتواند قابلیت نگهداری و گسترشپذیری نرمافزار شما را افزایش دهد. این الگو اشیا را از هم جدا کرده و نقطهای مرکزی برای منطق تعامل ایجاد میکند.
الگوی طراحی میانجی در صورت استفاده صحیح، بسیار انعطافپذیر و سودمند است. با این حال، شایان ذکر است که این الگو و دیگر الگوهای طراحی ارائهشده توسط Gang of Four، یک راهحل همهجانبه یا نهایی برای طراحی برنامهها نیستند. در نهایت، این وظیفه مهندسان است که تشخیص دهند چه زمانی از یک الگوی خاص استفاده کنند. این الگوها زمانی مفید هستند که به عنوان یک ابزار دقیق به کار گرفته شوند، نه به عنوان یک ابزار کلی و غیرهدفمند.