بررسی Rules engin design Patterns

چرا اصن باید بریم سراغ rule engin design pattern وقتی ما داخل منطق برنامه خودمون قراره کلی شرط تو در تو استفاده کنیم که باعث میشه پیچیدگی برنامه خودمون بره بالا اینجاست که این design pattern میاد و به داد ما میرسه اما خب وقتی توضیحات این پترن رو میخونید در مورد این پترن معمولا جمله زیر رو میگنیک پترن خوب برای از بین بردن پیچیدگی ها در یک منطقی که پر از شرطه و جایگزینی اون با یک کدی که قابل توسعه باش و ماژولار باشه این کاری هستش که این دیزاین پترن میاد و برای ما انجام میده اما برسیم به سوال اصلیاین Rules Engin Pattern دقیقا چی هستش؟ یک Rule engin در عمل مجموعه ای از قواعد یا همان Rule ها را درون خود نگه میدارد و این Rule ها رو بر روی یک context پیاده سازی میکنه تا یک نتجیه به دست بیاد پس شد Engin یک بخش این pattern و مجموعه Rule ها بخش دیگه این پترن است این نکته رو در نظر بگیرید که این دو بخش از هم دیگه جدا هستن پس در عمل Engin این شرط ها رو یا همان Rule ها رو بر روی یک context پیاده سازی میکنه و نتجیه رو به ما برمیگردونه.پس Rule ها مشخص است به یک شرط اشاره میکنن که باعث میشن یک Result برگردانده شود و خب این شرط ها در کنار همدیگه قرار میگیرند تا توسط Engin استفاده شود در ضمن این نکته رو هم به یاد داشته باشید که ممکنه به صورت ترکیبی و یا براساس یک Order خاص اینا اجرا بشن یا حتی فیلتر بشن و ممگن براساس دلایلی اصلا کنار گذاشته بشن. خب اما این بیاد بررسی کنیم تا ببینیم این الگو تو کدوم دسته بندی از این design patterns ها قرار میگیره اما قلبش بیاد ببینیم چند مدل design patterns داریم Design Pattern ها به 3 مدل دسته بندی میشن که به صورت خلاصه میشه:الگوهای creational الگوهای creational مسئله ی ایجاد شی رو بررسی می کنن. اینکه در هر شرایطی بهتره ایجاد شی رو به چه صورت انجام بدیم.الگوهای Structuralالگوهای Structural یا ساختاری روی مسئله ی ترکیب آبجکت ها و کلاس ها برای شکل دادن ساختار های بزرگ تر تمرکز دارن، به نحوی که قابلیت انعطاف و کارایی برنامه در حد امکان در بالاترین حد باشه.الگوهای Behavioral الگوهای طراحی Behavioral یا رفتاری روی بحث تقسیم وظایف بین کلاس های مختلف تمرکز دارن.خب پس ما شرط های پیچیده خودمون رو میخوایم بین کلاس های مختلف تقسیم کنیم و رفتار های متفاوت رو چک میکنه پس این pattern در دسته Behaviral ها قرار میگیره.کجا به درد میخوره برای مثال اینکه یه بانک میخواد به یکی وام بده میاد چندتا پارامتر رو در نظر میگیره طرف کار داره اگه داشته باشه میزان درامدش چقدره شغلش وابسته به چه ارگانی هستش و فیلتر فیلتر شرط شرط خب در نهایت میگه ایا طرف واجد شرایط وام هستش یا نه .اما چه مشکلاتی را میتونیم برطرف کنیم مشکل the open/closed principle مثلا شما میخواین یک Rule یا شرطی رو اضافه کنید نرید کدهای قدیمی رو تغییر بدید بلکه میاید یک Rule جدید رو اضافه میکنید اما بدنه این pattern چیزی شبیه به کد زیر هستشکه یک سری شرط داریم که این شرط همون Rule های ما هستش که از یک interface ارث بری میکنه و یک Engin داریم که لیست از این Rule هارو بهش پاس میدیم که باعث اجرا شدنش میشه اما هر Rule باید اولا یک خروجی ساده باشه و یک Rule یک کار بزرگ پیچیده رو انجام نده و خب اما وقتش رسیده بریم سراغ کد!!!تو این مثال ما سعی داریم مقدار کالری مورد نیاز برای بدن یک شخص رو بسته به یک سری اطلاعات به دست بیاریم بریم اول و یک کد کثیف رو ببینیمpublic class BadCalculateCode { public int CalculateCaloriesPercentage(BmrModel bmrModel) { int currentCalories = 0 ; if (bmrModel.Age >= 1 && bmrModel.Age <= 9) { currentCalories += 3; }else if (bmrModel.Age >= 10 && bmrModel.Age <= 20) { currentCalories += 4; } if (bmrModel.Gender == Gender.Male) { currentCalories += 1; } else { currentCalories += 2; } if (bmrModel.Weight < 60) { currentCalories += 7; } else { currentCalories += 8; } return currentCalories; } }خب یک کلاسی داریم به اسم BadCalculateCode که داخلش یک متد داره به اسم CalculateCaloriesPercentage که یک context ورودی اون هستش و میاد یک سری if رو روی این اعمال میکنه و در نتجیه یک مقدار برمیگردونه.شاید شما پیش خودتون بگید این که اسون و قابل فهمه خب این یک سناریو اسون با تعداد if های محدود هستش اما اگه همین تعداد if ها زیاد و تو در تو بشه اون موقعه برای فهمیدنش باید نصف انسان و نصف کامپایلر باشید تا بتونید سر در بیاد که این کد چیکار میکنه حالا اضافه کردن شرط جدید که دیگه هیچی!!اما خب بریم سراغ نوشتن یک کد تمیزاول خب که باید یک مدل داشته باشیم که میشه همون context ماpublic class BmrModel { public Gender Gender { get; set; } public int Age { get; set; } public int Height { get; set; } public int Weight { get; set; } }اول میایم یک interface تعریف میکنیم که همه شرط های ما یا Rule های باید از اون ارث بری کننکه این interface یک متد داره به اسم CalculateCaloriesPercentage که تو توضیحات بالا گفتیم که این rule ها بر روی یک context پیاده سازی میشوند که context ما اینجا BmrModel هستش و یک currentCalories که شرط های مختلف داریم که سبب تغییر currentCalories پس باید به هر rule این مقدار رو پاس بدیم تا بتونیم در پایان از اون استفاده کنیمpublic interface ICaloriesRule { int CalculateCaloriesPercentage(BmrModel bmrModel ,int currentCalories); }و بعد از این میام و شرط های خودمون رو مینویسم مثلا شرط مروبط به سنpublic class AgeRule : ICaloriesRule { public int CalculateCaloriesPercentage(BmrModel bmrModel, int currentCalories) { return bmrModel.Age switch { >= 1 and <= 9 => 3, >= 10 and <= 20 => 4, >= 21 and <= 30 => 5, >= 31 and <= 40 => 6, >= 41 => 6, _ => 0 }; } }شرط مربوط به جنسیتpublic class GenderRule : ICaloriesRule { public int CalculateCaloriesPercentage(BmrModel bmrModel, int currentCalories) { return bmrModel.Gender == Gender.Male ? 1 : 2; } }شرط مروبط به قدpublic class HeightRule : ICaloriesRule { public int CalculateCaloriesPercentage(BmrModel bmrModel, int currentCalories) { return bmrModel.Height switch { < 150 => 9, >= 150 => 10 }; } }و شرط مروبط به وزنpublic class WeightRule : ICaloriesRule { public int CalculateCaloriesPercentage(BmrModel bmrModel, int currentCalories) { return bmrModel.Weight switch { <= 60 => 7, > 60 => 8 }; } }و حالا باید engin خودمون رو بسازیم public class BmrRuleEngin { List<ICaloriesRule> rules = new(); public BmrRuleEngin(IEnumerable<ICaloriesRule> _rules) { rules.AddRange(_rules); } public int CalculateCaloriesPercentage(BmrModel bmrModel) { var currentCalories = 0; foreach (var rule in rules) { currentCalories = rule.CalculateCaloriesPercentage(bmrModel, currentCalories); } return currentCalories; } }که یک لیست از rule ها داره و یک constructor داره که همه rule ها به لیست ما اضافه میشه و یک متد داریم که BmrModel به اون داده میشه به وسیله for مقادیر محاسبه میشهخب حالا باید کلاسی بنویسیم که متدی داشته باشه تا بیاد و از این engin که ما نوشتیم استفاده بکنه public class BmrRuleCalculator { public int CalculateTaxPercentage(BmrModel bmrModel) { var ruleType = typeof(ICaloriesRule); var rules = GetType().Assembly.GetTypes() .Where(a => ruleType.IsAssignableFrom(a) && !a.IsInterface) .Select(q => Activator.CreateInstance(q) as ICaloriesRule).ToList(); var engin = new BmrRuleEngin(rules); return engin.CalculateCaloriesPercentage(bmrModel); } }که به وسیله این reflaction میاد از assmbly های سیستم استفاده میکنه و همه کلاس هایی که از ICaloriesRule ارث بری کردن و میگیره و اون رو به engin ما میده و خب این شد rule engin pattern مابرای تستش هم یک متد تست مینویسم public class CaloriesTest{ private BmrRuleCalculator _ruleCalculator = new(); [Fact] public void Person_Calories_Calculate() { //Arrange var bmrModel = new BmrModel { Age = 20, Gender = Gender.Male, Height = 180, Weight = 70 }; //Act var result = _ruleCalculator.CalculateCaloriesPercentage(bmrModel); //Assert Assert.Equal(8,result); }}اما خب یک وِیژگی خوبی که این pattern به ما میده این هستش که امکان کار گروهی رو فراهم میکنه برای مثال اگر تیم 3 نفر باشه میتونیم به تعداد مشخص Rule به هر برنامه نویس بدیم و بدون اینکه Rule هر برنامه نویس تداخل ایجاد کنه با Rule برنامه نویس دیگه و هرکس کار های خودش رو پیش ببره.امیدوارم که از این نوشته خوشتون اومده باشه.