توضیح دوات: برای راهنمایی و اطلاعات بیشتر در مورد نمودار کلاس به این مقاله مراجعه کنید.
الگوی طراحی Command یک الگوی رفتاری است که یک درخواست را به یک شیء مستقل تبدیل میکند که شامل تمام جزئیات آن درخواست است. این تبدیل به شما این امکان را میدهد که درخواستها را به عنوان آرگومانهای متد ارسال کنید، اجرای یک درخواست را به تأخیر بیندازید یا در صف قرار دهید و از عملیات قابل بازگشت پشتیبانی کنید.
میتوانید کد مثال این پست را در GitHub پیدا کنید.
مفهومسازی مشکل
فرض کنید ما در حال توسعه یک برنامه ویرایش متن جدید هستیم. هدف فعلی ما طراحی یک نوار ابزار با تعداد زیادی دکمه برای وظایف مختلف ویرایشگر است. یک کلاس Button مفید طراحی کردهایم که میتوان از آن برای دکمههای نوار ابزار و همچنین دکمههای عمومی در سایر دیالوگها استفاده کرد.
اگرچه این دکمهها شبیه به هم به نظر میرسند، اما اهداف متفاوتی دارند. کد مربوط به کلیک هر دکمه باید کجا ذخیره شود؟ واضحترین گزینه این است که زیرکلاسهایی برای هر محل دکمه ایجاد کنیم که کدی که هنگام کلیک دکمه اجرا میشود را در خود داشته باشند.
این روش معایب قابل توجهی دارد. یکی از مشکلات بزرگ این است که تعداد زیادی زیرکلاس تولید میشود. تغییرات در کلاس Button همچنان قابل مدیریت است، اما ممکن است کد این زیرکلاسها را خراب کند. به طور کلی، کد رابط کاربری شما بیش از حد به کد منطق کسبوکار وابسته میشود.
و اکنون به بدترین بخش میرسیم. برخی از وظایف، مانند انتشار یک پست، به فراخوانیهای متعددی نیاز دارند. به عنوان مثال، یک کاربر ممکن است روی یک دکمه کوچک “Publish” در نوار ابزار کلیک کند، چیزی را از منوی زمینه انتخاب کند، یا به سادگی ترکیب کلیدهای Ctrl+Shift+P را روی صفحه کلید فشار دهد.
وقتی برنامه ما فقط نوار ابزار داشت، قرار دادن پیادهسازی عملیاتهای مختلف در زیرکلاسهای دکمهها مشکلی نداشت. به عبارت دیگر، داشتن کد انتشار داخل زیرکلاس PublishButton
کافی بود. با این حال، زمانی که منوهای زمینه، میانبرها و سایر ویژگیها را اضافه کنید، یا باید کد عملیات را در چندین کلاس تکرار کنید یا منوها را به دکمهها وابسته کنید که گزینه بدتری است.
طراحی خوب نرمافزار اغلب بر پایه روش جداسازی نگرانیها استوار است که معمولاً به تقسیم یک برنامه به لایهها منجر میشود. واضحترین مثال یک لایه رابط کاربری گرافیکی و یک لایه منطق کسبوکار است. لایه رابط کاربری مسئول نمایش تصاویر زیبا روی صفحه، دریافت ورودی و نمایش نتایج کاربر و برنامه است. در انجام کارهای مهمی مانند محاسبه مسیر ماه یا نوشتن گزارش سالانه، لایه رابط کاربری وظیفه را به لایه زیری منطق کسبوکار محول میکند.
در کد، این میتواند به این شکل باشد: یک شیء رابط کاربری یک متد از یک شیء منطق کسبوکار را فراخوانی میکند و آرگومانهایی به آن میدهد. این معمولاً به عنوان ارسال یک درخواست از یک بخش به بخش دیگر تعریف میشود.
طبق الگوی طراحی Command، اشیاء رابط کاربری نباید این درخواستها را مستقیماً ارسال کنند. در عوض، باید تمام دادههای درخواست مانند شیء فراخوانیشده، نام متد و لیست آرگومانها را در یک کلاس فرمان جداگانه با یک تابع واحد که این درخواست را راهاندازی میکند، ایزوله کنیم.
اشیاء فرمان عناصر مختلف رابط کاربری و منطق کسبوکار را به هم متصل میکنند. دیگر نیازی نیست که شیء رابط کاربری بداند کدام شیء منطق کسبوکار درخواست را دریافت میکند و چگونه پردازش میشود. شیء رابط کاربری فقط فرمان را راهاندازی میکند که تمام جزئیات را مدیریت میکند.
گام بعدی اطمینان از این است که تمام دستورات ما از یک رابط مشترک استفاده کنند. این معمولاً یک متد اجرای واحد است که هیچ پارامتری نمیگیرد. این رابط به ما اجازه میدهد تا از دستورات مختلف با یک ارسالکننده درخواست یکسان استفاده کنیم بدون اینکه به کلاسهای فرمان خاص متصل شویم. به عنوان یک مزیت اضافی، اکنون میتوانیم اشیاء فرمان مرتبط با ارسالکننده را تغییر دهیم و در نتیجه رفتار آن را در زمان اجرا تغییر دهیم.
شاید متوجه شده باشید که یک قطعه از پازل گم شده است: پارامترهای درخواست. یک شیء رابط کاربری ممکن است پارامترهایی به شیء لایه کسبوکار داده باشد. اگر متد اجرای فرمان هیچ پارامتری نداشته باشد، چگونه دادههای درخواست را به گیرنده ارائه میکنیم؟ مشخص میشود که فرمان باید یا از قبل با این اطلاعات پیکربندی شده باشد یا قادر به دریافت آن به طور مستقل باشد.
بیایید به ویرایشگر متن خود برگردیم. پس از اعمال الگوی Command، دیگر نیازی به همه این زیرکلاسهای دکمه برای پیادهسازی اقدامات مختلف کلیک نداریم. کافی است یک فیلد به کلاس پایه Button اضافه کنیم که یک مرجع به یک شیء فرمان ذخیره کند و به دکمه دستور دهد که هنگام کلیک آن فرمان را اجرا کند.
ما مجموعهای از کلاسهای فرمان برای هر عملکرد ممکن ایجاد خواهیم کرد و آنها را بر اساس رفتار مورد نظر به دکمههای خاص اختصاص میدهیم.
منوها، میانبرها و دیالوگهای کامل، برای مثال، میتوانند به همین روش پیادهسازی شوند. آنها به یک فرمان مرتبط خواهند شد که هنگام تعامل کاربر با عنصر رابط کاربری اجرا خواهد شد. همانطور که میتوانید حدس بزنید، عناصر مرتبط با همان عملکردها به همان دستورات مرتبط خواهند بود و از تکرار کد جلوگیری میکنند.
در نتیجه، دستورات به عنوان یک لایه میانی مفید عمل میکنند و ارتباط بین سطوح رابط کاربری و منطق کسبوکار را کاهش میدهند. و این فقط بخشی از آن چیزی است که الگوی Command ارائه میدهد.
ساختار الگوی طراحی Command
در پیادهسازی پایه، الگوی طراحی Command پنج بخش اصلی دارد:
- Invoker: درخواستها توسط کلاس Invoker شروع میشوند. این کلاس باید یک فیلد داشته باشد تا یک مرجع به یک شیء فرمان را ذخیره کند. به جای ارسال مستقیم درخواست به گیرنده، Invoker آن فرمان را راهاندازی میکند. لازم به ذکر است که Invoker فرمان را ایجاد نمیکند، بلکه یک فرمان از پیش ساختهشده را از طریق سازنده از کلاینت دریافت میکند.
- Command: رابط Command یک متد واحد برای اجرای فرمان را اعلام میکند.
- Concrete Commands: انواع مختلف درخواستها از طریق Concrete Commands پیادهسازی میشوند. یک فرمان خاص کار را به تنهایی انجام نمیدهد؛ بلکه فقط درخواست را به یکی از کلاسهای منطق کسبوکار ارسال میکند. فیلدهایی در فرمان خاص میتوانند برای نگهداری پارامترهای مورد نیاز برای اجرای یک متد روی اشیاء منطق کسبوکار اعلام شوند. ما میتوانیم این اشیاء را غیرقابل تغییر کنیم با اجازه دادن فقط به مقداردهی اولیه مبتنی بر سازنده.
- Receiver: برخی منطق کسبوکار در کلاس Receiver قرار دارد. تقریباً هر شیئی میتواند به عنوان گیرنده استفاده شود. اکثر دستورات فقط به مکانیک نحوه ارسال یک درخواست به گیرنده میپردازند و انجام کار واقعی را به گیرنده واگذار میکنند.
- Client: کلاینت مسئول ایجاد و پیکربندی اشیاء فرمان خاص است. کلاینت باید تمام پارامترهای درخواست، از جمله یک نمونه گیرنده، را به سازنده فرمان ارائه دهد. فرمان نتیجهشده سپس میتواند به یک یا چند ارسالکننده مرتبط شود.
برای نشان دادن نحوه کار الگوی طراحی Command، میخواهیم یک مثال واقعی پیادهسازی کنیم. فرض کنید میخواهیم یک ابزار سیستم فایل ارائه دهیم که شامل متدهایی برای باز کردن، نوشتن و بستن فایلها باشد. این برنامه سیستم فایل باید با انواع مختلف سیستمعامل، از جمله Windows و Unix، سازگار باشد.
ابتدا میخواهیم کلاسهای Receiver خود را پیادهسازی کنیم. ابتدا یک رابط IFileSystemReceiver
تعریف خواهیم کرد که پایه همه گیرندههای ما خواهد بود. به یاد داشته باشید که شرکتکنندگان Receiver ما با پیادهسازی واقعی منطق کسبوکار سروکار دارند.
public interface IFileSystemReceiver
{
public void OpenFile();
public void WriteFile();
public void CloseFile();
}
رابط ما سه متد را تعریف میکند: برای باز کردن، نوشتن و بستن یک فایل. حالا که رابط خود را داریم، قصد داریم کلاسهای دریافتکنندهی Concrete خود را پیادهسازی کنیم. ابتدا، UnixFileSystemReceiver
:
public class UnixFileSystemReceiver : IFileSystemReceiver
{
public void OpenFile()
{
Console.WriteLine("Opening file in Unix OS");
}
public void WriteFile()
{
Console.WriteLine("Writing file in Unix OS");
}
public void CloseFile()
{
Console.WriteLine("Closing file in Unix OS");
}
}
و کلاس WindowsFileSystemReceiver
:
public class WindowsFileSystemReceiver : IFileSystemReceiver
{
public void OpenFile()
{
Console.WriteLine("Opening file in Windows OS");
}
public void WriteFile()
{
Console.WriteLine("Writing file in Windows OS");
}
public void CloseFile()
{
Console.WriteLine("Closing file in Windows OS");
}
}
مرحله بعدی تعریف شرکتکننده Command است. شرکتکننده Command معمولاً یک متد برای اجرای فرمان تعریف میکند. در اینجا، ما رابط ICommand
خود را تعریف خواهیم کرد:
public interface ICommand
{
public void Execute();
}
اکنون باید پیادهسازیهایی برای هر نوع عملی که توسط دریافتکننده انجام میشود، بنویسیم. چون سه عمل داریم، سه پیادهسازی (ConcreteCommand) ایجاد خواهیم کرد. هر پیادهسازی Concrete Command درخواست را به متد مناسب دریافتکننده هدایت میکند. ابتدا، OpenFileCommand
:
public class OpenFileCommand : ICommand
{
private readonly IFileSystemReceiver _fileSystemReceiver;
public OpenFileCommand(IFileSystemReceiver fileSystemReceiver)
{
_fileSystemReceiver = fileSystemReceiver;
}
public void Execute()
{
_fileSystemReceiver.OpenFile();
}
}
در مرحله بعد، CloseFileCommand
:
public class CloseFileCommand : ICommand
{
private readonly IFileSystemReceiver _fileSystemReceiver;
public CloseFileCommand(IFileSystemReceiver fileSystemReceiver)
{
_fileSystemReceiver = fileSystemReceiver;
}
public void Execute()
{
_fileSystemReceiver.CloseFile();
}
}
و در نهایت WriteFileCommand
:
public class WriteFileCommand : ICommand
{
private readonly IFileSystemReceiver _fileSystemReceiver;
public WriteFileCommand(IFileSystemReceiver fileSystemReceiver)
{
_fileSystemReceiver = fileSystemReceiver;
}
public void Execute()
{
_fileSystemReceiver.WriteFile();
}
}
حالا که پیادهسازیهای دریافتکننده و فرمان را کامل کردهایم، میتوانیم به کلاس فراخوان (Invoker) برویم. بهجای ارسال مستقیم درخواست به دریافتکننده، فراخوان فرمان را آغاز میکند. باید توجه داشت که ارسالکننده مسئول ساختن شیء فرمان نیست. در ادامه کلاس FileInvoker
ما آمده است:
public class FileInvoker
{
private readonly ICommand _command;
public FileInvoker(ICommand command)
{
this._command = command;
}
public void Execute()
{
_command.Execute();
}
}
حالا که پیادهسازی فرمان خود را کامل کردهایم، میتوانیم به نوشتن یک برنامه ساده مشتری بپردازیم. اما ابتدا باید یک متد کاربردی برای ایجاد اشیای مورد نیاز IFileSystemReceiver
فراهم کنیم. این متد سیستمعامل زیرساخت را بررسی کرده و تصمیم میگیرد که کدام زیرکلاس از IFileSystemReceiver
را نمونهسازی کند:
public class FileSystemReceiverUtils
{
public static IFileSystemReceiver GetFileSystemReceiver()
{
var osName = System.Runtime.InteropServices.RuntimeInformation.OSDescription;
Console.WriteLine($"Underlying OS: {osName}");
if (osName.Contains("Windows"))
return new WindowsFileSystemReceiver();
return new UnixFileSystemReceiver();
}
}
در نهایت، بیایید شرکتکننده Client خود را پیادهسازی کنیم:
var fs = FileSystemReceiverUtils.GetFileSystemReceiver();
var openFileCommand = new OpenFileCommand(fs);
var invoker = new FileInvoker(openFileCommand);
invoker.Execute();
var writeFileCommand = new WriteFileCommand(fs);
invoker = new FileInvoker(writeFileCommand);
invoker.Execute();
var closeFileCommand = new CloseFileCommand(fs);
invoker = new FileInvoker(closeFileCommand);
invoker.Execute();
مزایا و معایب الگوی طراحی Command
مزایا
- کلاسهایی که عملیات را فراخوانی میکنند میتوانند از کلاسهایی که این عملیات را اجرا میکنند جدا شوند و در نتیجه اصل مسئولیتپذیری واحد (Single Responsibility Principle) رعایت میشود.
- میتوانیم دستورات جدیدی به برنامه اضافه کنیم بدون اینکه کد کلاینت موجود را تحت تأثیر قرار دهیم، بنابراین اصل باز/بسته (Open/Closed Principle) رعایت میشود.
- میتوانیم اجرای عملیاتها را به تأخیر بیندازیم.
معایب
- از آنجا که یک لایه جدید بین ارسالکنندگان و گیرندگان معرفی میکنیم، کد ممکن است پیچیدهتر شود.
ارتباط با الگوهای دیگر
الگوهای Chain of Responsibility، Command، Mediator و Observer چندین روش برای اتصال ارسالکنندگان و گیرندگان درخواستها را پوشش میدهند:
- Chain of Responsibility یک درخواست را به صورت متوالی از طریق زنجیرهای پویا از گیرندگان احتمالی هدایت میکند تا زمانی که توسط یکی از آنها پردازش شود.
- Command روابط یکطرفه بین ارسالکنندگان و گیرندگان ایجاد میکند.
- Mediator اتصالات مستقیم بین ارسالکنندگان و گیرندگان را حذف میکند و آنها را مجبور میکند از طریق یک شیء واسط ارتباط برقرار کنند.
- گیرندگان میتوانند به صورت پویا برای دریافت درخواستها در Observer مشترک شوند یا از آن لغو اشتراک کنند.
دستورات میتوانند به عنوان پردازندهها در Chain of Responsibility پیادهسازی شوند. در این حالت، میتوانید عملیاتهای متنوعی را بر روی همان شیء زمینه انجام دهید که با یک درخواست نمایش داده میشود. با این حال، رویکرد دیگری وجود دارد که در آن درخواست به خودی خود یک شیء Command است. در این حالت، میتوانید همان عملیات را در زنجیرهای از زمینههای مختلف اجرا کنید.
هنگام پیادهسازی “بازگشت” (undo)، میتوانید از Command و Memento به طور مشترک استفاده کنید. در این حالت، دستورات مسئول انجام عملیاتهای مختلف روی یک شیء هدف هستند، در حالی که mementos وضعیت شیء را درست قبل از اجرای یک دستور ثبت میکنند.
از آنجا که هر دو الگو Command و Strategy میتوانند برای پارامتردهی یک شیء با یک عملیات استفاده شوند، ممکن است مشابه به نظر برسند. با این حال، اهداف آنها کاملاً متفاوت است:
- Command هر عملیاتی را میتواند به یک شیء تبدیل کند. پارامترهای فرآیند به فیلدهای آن شیء تبدیل میشوند. این تبدیل به شما امکان میدهد اجرای عملیات را به تعویق بیندازید، در صف قرار دهید، تاریخچه دستورات را ذخیره کنید، دستورات را به خدمات دور ارسال کنید و غیره.
- Strategy، از سوی دیگر، معمولاً روشهای مختلفی برای دستیابی به یک هدف ارائه میدهد و به شما امکان میدهد این الگوریتمها را در یک کلاس زمینهای تغییر دهید.
وقتی نیاز به ذخیره نسخههایی از Commands در تاریخچه دارید، Prototype میتواند مفید باشد.
Visitor میتواند به عنوان نسخهای قدرتمندتر از الگوی Command در نظر گرفته شود. اشیاء آن میتوانند عملیاتهایی روی اشیاء کلاسهای مختلف انجام دهند.
نتیجهگیری
در این مقاله، در مورد الگوی طراحی Command، زمان استفاده از آن و مزایا و معایب استفاده از این الگوی طراحی بحث کردیم. سپس برخی موارد استفاده از این الگو و نحوه ارتباط Command با سایر الگوهای طراحی کلاسیک را بررسی کردیم.
شایان ذکر است که الگوی Command، همراه با سایر الگوهای طراحی ارائهشده توسط Gang of Four، راهحل جامع یا کاملی برای طراحی یک برنامه نیست. این مهندسین هستند که باید تصمیم بگیرند چه زمانی از یک الگوی خاص استفاده کنند. در نهایت، این الگوها زمانی مفید هستند که به عنوان ابزاری دقیق استفاده شوند، نه یک ابزار ضربهای.