الگوی Command، یک الگوی طراحی رفتاری است که میتوانیم از آن برای تبدیل یک request به یک آبجکت که شامل تمام اطلاعات مربوط به آن request است استفاده کنیم.
الگوی طراحی Command در #C کاملا محبوب است، به خصوص زمانی که می خواهیم اجرای یک request را به تأخیر بیندازیم یا در صف قرار دهیم یا زمانی که می خواهیم عملیات خود را ردگیری کنیم. علاوه بر این، این امکان ردگیری کردن، امکان undo کردن عملیات را نیز به ما میدهد.
این مقاله، بخشی از مجموعه آموزشی زیر است:
- الگوی طراحی Builder و Fluent Builder
- اینترفیس Fluent Builder به همراه Generic بازگشتی
- Facated Builder
- متد Factory
- Singleton
- Adapter
- Composite
- Decorator
- Command
- Strategy
- Facade
سورس کد در این لینک موجود است: الگوی طراحی Command – سورس کد.
برای مشاهده لیست کامل مقالات این مجموعه آموزشی، الگوهای طراحی #C را بررسی کنید.
این مقاله، به قسمتهای زیر تقسیم میشود:
پیاده سازی الگوی طراحی Command
الگوی طراحی Command، شامل کلاس Invoker، کلاس/اینترفیس Command، کلاسهای Concrete command و کلاس گیرنده (Receiver) میباشد. با این تفسیر، در مثال خود، ما از همین ساختار طراحی پیروی می کنیم.
بنابراین، کاری که میخواهیم انجام دهیم این است که یک برنامه ساده بنویسیم که در آن قیمت محصول را تغییر دهیم که الگوی طراحی Command را پیادهسازی کند.
همانطور که گفته شد، بیایید با کلاس گیرنده محصول شروع کنیم، که باید business logic پایه را در برنامه ما دربر داشته باشد:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
public class Product { public string Name { get; set; } public int Price { get; set; } public Product(string name, int price) { Name = name; Price = price; } public void IncreasePrice(int amount) { Price += amount; Console.WriteLine($"The price for the {Name} has been increased by {amount}$."); } public void DecreasePrice(int amount) { if(amount < Price) { Price -= amount; Console.WriteLine($"The price for the {Name} has been decreased by {amount}$."); } } public override string ToString() => $"Current price for the {Name} product is {Price}$."; } |
بنابراین این کلاس receiver ما است که کاملا یک منطق ساده و واضح دارد. ما اینجا فقط قیمت محصول را افزایش یا بر حسب شرط کاهش میدهیم. در آخر، ما متد ToString
را داریم که قادر به چاپ و نمایش اطلاعات آبجکت ما است.
حالا کلاس Client میتواند کلاس Product
را نمونه سازی کرده و action های مورد نیاز را اجرا کند. اما فرض الگوی طراحی Command بر این اساس است که ما بهتر است از کلاس receiver به طور مستقیم استفاده نکنیم. بلکه بهتر است تمام جزییات request را به داخل یک کلاس ویژه به نام Command استخراج کنیم.
و این دقیقا کاری است که ما قصد داریم انجام دهیم.
اولین کاری که ما میخواهیم انجام دهیم اضافه کردن اینترفیس ICommand
است:
1 2 3 4 |
public interface ICommand { void ExecuteAction(); } |
فقط برای تعیین نمودن اقدامات اصلاح قیمت خود، میخواهیم یک شمارنده ساده به نام PriceAction اضافه کنیم:
1 2 3 4 5 |
public enum PriceAction { Increase, Decrease } |
در آخر، کلاس ProductCommand
را اضافه میکنیم:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
public class ProductCommand : ICommand { private readonly Product _product; private readonly PriceAction _priceAction; private readonly int _amount; public ProductCommand(Product product, PriceAction priceAction, int amount) { _product = product; _priceAction = priceAction; _amount = amount; } public void ExecuteAction() { if(_priceAction == PriceAction.Increase) { _product.IncreasePrice(_amount); } else { DecreasePrice(_amount); } } } |
همانطور که میبینیم، کلاس ProductCommand
تمام اطلاعات مربوط به request مورد نظر را در اختیار دارد و بر اساس آن، action مورد نیاز را اجرا میکند.
در ادامه، کلاس ModifyPrice
را اضافه میکنیم که به عنوان Invoker عمل میکند:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
public class ModifyPrice { private readonly List<ICommand> _commands; private ICommand _command; public ModifyPrice() { _commands = new List<ICommand>(); } public void SetCommand(ICommand command) => _command = command; public void Invoke() { _commands.Add(_command); _command.ExecuteAction(); } } |
این کلاس میتواند با هر command که اینترفیس ICommand
را پیاده سازی میکند کار کند و تمام عملیات را نیز ذخیره کند.
حالا ما میتوانیم کار با قسمت client را شروع کنیم:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
class Program { static void Main(string[] args) { var modifyPrice = new ModifyPrice(); var product = new Product("Phone", 500); Execute(product, modifyPrice, new ProductCommand(product, PriceAction.Increase, 100)); Execute(product, modifyPrice, new ProductCommand(product, PriceAction.Increase, 50)); Execute(product, modifyPrice, new ProductCommand(product, PriceAction.Decrease, 25)); Console.WriteLine(product); } private static void Execute(Product product, ModifyPrice modifyPrice, ICommand productCommand) { modifyPrice.SetCommand(productCommand); modifyPrice.Invoke(); } } |
نتیجه باید به صورت زیر باشد:
عالی. ما اینجا میتوانیم ترتیب اجرای action ها و قیمت درست بعد از اصلاحیه ها را ببینیم.
با در نظر گرفتن این موضوع که ما action های خود را در کلاس Invoker ردگیری میکنیم، میتوانیم در صورت نیاز، از آن برای Undo کردن عملیات خود نیز استفاده کنیم.
بنابراین آن را امتحان میکنیم.
پیاده سازی عملیات Undo در الگوی طراحی Command
برای پیاده سازی عملیات Undo، بیایید با تغییرات اینترفیس ICommand
شروع کنیم:
1 2 3 4 5 |
public interface ICommand { void ExecuteAction(); void UndoAction(); } |
سپس کلاس ProductCommand
را با اضافه کردن متد UndoAction
تغییر میدهیم:
1 2 3 4 5 6 7 8 9 10 11 |
public void UndoAction() { if (_priceAction == PriceAction.Increase) { _product.DecreasePrice(_amount); } else { _product.IncreasePrice(_amount); } } |
البته ما باید با اضافه کردن متد UndoActions
، کلاس ModifyPrice
را نیز اصلاح کنیم:
1 2 3 4 5 6 7 |
public void UndoActions() { foreach (var command in Enumerable.Reverse(_commands)) { command.UndoAction(); } } |
لطفاً توجه داشته باشید که ما از متد Linq Reverse استفاده نمی کنیم بلکه از ()Enumerable.Reverse استفاده می کنیم. این به این دلیل است که متد Linq لیست ما را تغییر می دهد و ما نمی خواهیم این اتفاق بیفتد. تنها چیزی که می خواهیم فقط یک لیست معکوس است اما بدون تغییر در خود لیست اصلی.
حال، وقتی کلاس Client متد UndoActions را فراخوانی میکند، تمام عملیاتهای درون list را پیمایش میکند و عملیات متضاد با عملیات انجام شده قبلی را اجرا میکند.
اجازه دهید تا آن را امتحان کنیم:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
class Program { static void Main(string[] args) { var modifyPrice = new ModifyPrice(); var product = new Product("Phone", 500); Execute(product, modifyPrice, new ProductCommand(product, PriceAction.Increase, 100)); Execute(product, modifyPrice, new ProductCommand(product, PriceAction.Increase, 50)); Execute(product, modifyPrice, new ProductCommand(product, PriceAction.Decrease, 25)); Console.WriteLine(product); Console.WriteLine(); modifyPrice.UndoActions(); Console.WriteLine(product); } private static void Execute(Product product, ModifyPrice modifyPrice, ICommand productCommand) { modifyPrice.SetCommand(productCommand); modifyPrice.Invoke(); } } |
نتیجه:
همه چیز طبق انتظار به درستی کار میکند.
بهبود Solution
ما الگوی طراحی Command را به داخل برنامه خود پیاده سازی کردیم و هیچ مشکل خاصی در آن وجود ندارد. اما یک نقص در Solution ما وجود دارد که مربوط به الگوی Command نمیشود، بلکه در کل مربوط به business logic ما میشود.
برای مثال، اگر کاهش قیمت را از 25 به 2500 تغییر میدادیم، چه اتفاقی می افتاد؟ خب، ما اعتبارسنجی مربوطه را در متد DecreasePrice
انجام داده ایم و از این رو، این اعتبارسنجی باعث میشود که این نوع کاهش قیمت، در نتیجه تاثیری نداشته باشد و حق با شماست. اما بر روی Undo action ها تاثیر میگذارد.
ببینیم که چطور این تاثیر را میگذارد:
1 2 3 4 5 |
... Execute(product, modifyPrice, new ProductCommand(product, PriceAction.Decrease, 2500)); ... |
نتیجه به این صورت خواهد بود:
همانطور که می بینید، قیمت ما کاهش نیافته است، اما عملیات در فروشگاه ما، به عنوان یک عملیات انجام شده ثبت شده است که باعث شده تا عمل Undo برای آن انجام شود که نباید این اتفاق می افتاد.
بنابراین، بیایید آن را برطرف کنیم.
اولین کاری که قرار است انجام دهیم اصلاح متد DecreasePrice
در کلاس Product
است:
1 2 3 4 5 6 7 8 9 10 |
public bool DecreasePrice(int amount) { if(amount < Price) { Price -= amount; Console.WriteLine($"The price for the {Name} has been decreased by {amount}$."); return true; } return false; } |
حالا میتوانیم کلاس ProductCommand
را نیز اصلاح کنیم:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
public class ProductCommand : ICommand { private readonly Product _product; private readonly PriceAction _priceAction; private readonly int _amount; public bool IsCommandExecuted { get; private set; } public ProductCommand(Product product, PriceAction priceAction, int amount) { _product = product; _priceAction = priceAction; _amount = amount; } public void ExecuteAction() { if(_priceAction == PriceAction.Increase) { _product.IncreasePrice(_amount); IsCommandExecuted = true; } else { IsCommandExecuted = _product.DecreasePrice(_amount); } } public void UndoAction() { if (!IsCommandExecuted) return; if (_priceAction == PriceAction.Increase) { _product.DecreasePrice(_amount); } else { _product.IncreasePrice(_amount); } } } |
و تمام. حال اگر برنامه خود را با مقدار کاهش 2500 اجرا کنیم، نتیجه درست خواهد بود:
همه چیز به خوبی پیش رفت.
نتیجه گیری
اگرچه الگوی طراحی Command باعث ایجاد پیچیدگی در کد ما میشود، اما می تواند بسیار مفید باشد.
با آن میتوانیم کلاسهایی را که عملیات را invoke میکنند از کلاسهایی که این عملیات را انجام میدهند جدا کنیم. علاوه بر این، اگر بخواهیم command های جدیدی را معرفی کنیم، لازم نیست کلاسهای موجود را تغییر دهیم. در عوض، ما فقط می توانیم این کلاس های command جدید را به پروژه خود اضافه کنیم.