فرض کنید می خواهید برنامه ای بنویسید که لیستی از اعداد را به عنوان ورودی دریافت کند، به روش حبابی مرتب کند و خروجی را برگرداند.
public int[] BubbleSort(int[] arr) { int temp = 0; for (int write = 0; write < arr.Length; write++) { for (int sort = 0; sort < arr.Length - 1; sort++) { if (arr[sort] > arr[sort + 1]) { temp = arr[sort + 1]; arr[sort + 1] = arr[sort]; arr[sort] = temp; } } } return arr; }
مرتب سازی را به انتخاب خود بر اساس صعودی انجام دادیم. اما کارفرما بعد از انجام کار از ما می خواهد که مرتب سازی بر اساس نزولی باید انجام شود. و بعد از مدتی دوباره می خواهد که مرتب سازی را صعودی کنید.
بر اساس کد هایی که تا این لحظه نوشته ایم باید هر بار سورس خود را تغییر دهیم. اگر با اصل open close principle آشنایی داشته باشید کد های ما باید برای توسعه باز و برای تغییر بسته باشد. من نباید برای تبدیل صعودی به نزولی دست به اصل سورس های نوشته شده بزنم. بلکه قسمتی که قرار است تغییر کند باید به شکل دیگری عوض شود. تا این لحظه کد ما در مقابل تغییر مصون نبود.
من در تابع Bubble Sort خود می خواهم جلوی تغییر کردن را بگیرم. در گام اول یک پارامتر boolean اضافه می کنم. با اضافه کردن این پارامتر از این پس نیاز نیست سورس خودم را عوض کنم و کلاینت برای تغییر ترتیب مرتب سازی به من true یا false ارسال می کند.
public static int[] BubbleSort(int[] arr, bool isAcs) { int temp = 0; for (int write = 0; write < arr.Length; write++) { for (int sort = 0; sort < arr.Length - 1; sort++) { if (isAcs && arr[sort] > arr[sort + 1] || !isAcs && arr[sort] < arr[sort + 1]) { temp = arr[sort + 1]; arr[sort + 1] = arr[sort]; arr[sort] = temp; } } } return arr; }
اما من با این کار open close principle را رعایت نکردم بلکه صورت مسئله را پاک کردم. مسئله دیگر این است که متد من single responsible هم نیست یعنی فقط یک کار انجام نمی دهد. دو کاری که انجام می دهد یکی تصمیم گیری در مورد صعودی یا نزولی کردن است و دیگری صعودی یا نزولی مرتب کردن است. برای حل کردن این موارد ما باید مسئولیت های اضافه را از کد های خود خارج کنیم.
یکی از راهکارها برای خارج کردن مسئولیت های اضافه، استفاده از delegate می باشد.
در حقیقت delegateها یکسری اشاره گر به تابع و متغیری از جنس تابع می باشند. ما یک delegate تعریف می کنیم که ساختار تابع را مشخص می کند. خروجی و تعداد ورودی های یک تابع، ساختار یا signature تابع می باشند. کاری که می خواهیم انجام دهیم این است که برای قسمت تصمیم گیری تابع نوشته شده یک signature تعریف کنیم. signature ما این است که دو پارامتر int ورودی می گیرد و یک boolean بر می گرداند.
برای تعریف delegate از کلمه کلیدی delegate استفاده می کنیم.
delegate bool SortDirection(int input01, int input02);
من delegate خود را تعریف می کنم و به عنوان ورودی به تابع خود ارسال می کنم.
public static int[] BubbleSort(int[] arr, SortDirection sortDirection) { int temp = 0; for (int write = 0; write < arr.Length; write++) { for (int sort = 0; sort < arr.Length - 1; sort++) { if (sortDirection.Invoke(arr[sort], arr[sort + 1])) { temp = arr[sort + 1]; arr[sort + 1] = arr[sort]; arr[sort] = temp; } } } return arr; }
از بیرون هر پیاده سازی قابلیت ارسال به پارامتر sortDirection را دارد.
و سپس توابع آن را تعریف می کنم.
public static bool Asc(int input01, int input02) { return input01 > input02; } public static bool Desc(int input01, int input02) { return input01 < input02; } static void Main(string[] args) { int[] arr = { 800, 11, 50, 447, 982, 225, 8 }; var sortedArr = BubbleSort(arr, Asc); }
پس از این مسئولیت تصمیم گیری داخل تابع من انجام نمی شود. جاهایی که می بینید در تابع logic های مختلف دارید که قابلیت تغییر دارند می توانید از این امکان استفاده کنید. کلاینت تصمیم می گیرد که این کار چگونه انجام شود.
نکته ای که در مورد delegate وجود دارد این است که شما می توانید از جنس delegate متغیر تعریف کنید و هر دو تابع را داخل یک متغیر قرار دهید. اصطلاحا به آن multi cast delegation می گویند.
SortDirection myDirection = Asc; myDirection += Desc; var result = myDirection.Invoke(1, 3); Console.Write(result); Console.ReadKey();
حالا اگر delegate را اجرا کنم هر دو متد پشت سر هم اجرا می شود. ممکن است در برنامه ها به شما بگویند که اگر این اتفاق افتاد کار یک انجام شود، اگر این یکی اتفاق افتاد کار دو هم انجام شود. بیست کار مختلف است که در شرایط مختلف حالت های مختلفی از آنها قابلیت اجرا را دارد. در if و else ها می توانیم یک multi cast delegate بسازیم و در نهایت آن را اجرا کنیم.
نکته ای که در multi cast delegate وجود دارد این است که من آخرین خروجی را می بینیم. به خاطر همین می گویند زمانی از آن استفاده کنید که خروجی شما void باشد وگرنه شما فقط به آخرین خروجی دسترسی خواهید داشت. اما اگر بخواهیم در این شرایط از دست دادن دیتا را نداشته باشیم از get invocation list استفاده می کنیم. با این شرایط بهتر است از try catch نیز استفاده کنیم.
var list = myDirection.GetInvocationList(); foreach (var item in list) { try { Console.WriteLine(item.DynamicInvoke(1, 3)); } catch {} } Console.ReadKey();
ایرادی که در این مدل کد نویسی وجود دارد، حجم زیادی از delegateهایی است که مجبوریم تعریف کنیم. یعنی به ازای هر signature باید یک delegate تعریف کنیم. مایکروسافت متوجه این مشکل شده است و دو تایپ Func و Action را ارائه کرده است. Actionها delegateهایی که هستند که صفر تا شانزده ورودی دارند و خروجی ندارند. و functionها delegateهایی هستند که هم ورودی و هم خروجی دارند.
public static int[] BubbleSortV4(int[] arr, Func<int, int, bool> sortDirection) { .... }
ایرادی دیگری که وجود دارد بعد از مدتی سورس ما پر از توابعی می شود که فقط برای ارسال به عنوان delegate به جایی ارسال می شوند و جای دیگری مورد استفاده ندارند. برای رفع این مشکل از امکانی به نام anonymous method استفاده می کنیم. anonymous method توابعی هستند که اسم ندارند.
static void Main(string[] args) { Func<int, int, bool> myFunc = delegate (int a, int b) { return a > b; }; int[] arr = { 800, 11, 50, 447, 982, 225, 8 }; var result = BubbleSort(arr, myFunc); for (int i = 0; i < result.Length; i++) { Console.Write(result[i] + " "); } Console.ReadKey(); }
ایرادی که در ساختار anonymous method وجود دارد این است که خوانایی آن یه مقدار سخت می باشد.
خیلی از مواردی که نوشته شده می تواند حذف شود مثل عبارت delegate و نوع پارامتر ها. به این فرمت اصلاحا lambda expression می گویند. هیچ تعریفی نمی خواهد و می تواند صفر، یک یا n ورودی داشته باشد.
Func<int, int, bool> myFunc = (a,b) => { return a > b; };
پایان