مقدمه
چرا باید از Rule Engine Design Pattern استفاده کنیم؟ وقتی در منطق برنامه خود از شرطهای تو در تو استفاده میکنیم، پیچیدگی برنامه افزایش مییابد. در اینجا، این الگوی طراحی به کمک ما میآید. با مطالعه این الگو، متوجه میشوید که جملهای شبیه به زیر در مورد آن بیان میشود:
"یک الگوی خوب برای از بین بردن پیچیدگیها در یک منطق پر از شرط و جایگزینی آن با کدی که قابل توسعه و ماژولار باشد."
یک Rule Engine در عمل مجموعهای از قواعد یا همان Ruleها را درون خود نگه میدارد و این Ruleها را بر روی یک context پیادهسازی میکند تا یک نتیجه به دست آید. پس، Engine یک بخش از این الگو و مجموعه Ruleها بخش دیگر این الگو هستند. این نکته را در نظر بگیرید که این دو بخش از هم جدا هستند. در عمل، Engine این شرطها یا همان Ruleها را بر روی یک context پیادهسازی کرده و نتیجه را به ما برمیگرداند.
پس Rule ها مشخص است به یک شرط اشاره میکنن که باعث میشن یک Result برگردانده شود و خب این شرط ها در کنار همدیگه قرار میگیرند تا توسط engine استفاده شود در ضمن این نکته رو هم به یاد داشته باشید که ممکنه به صورت ترکیبی و یا براساس یک Order خاص اینا اجرا بشن یا حتی فیلتر بشن و ممکن براساس دلایلی اصلا کنار گذاشته بشوند.
خب اما این بیاد بررسی کنیم تا ببینیم این الگو تو کدوم دسته بندی از این design patterns ها قرار میگیره اما قلبش بیاد ببینیم چند مدل design patterns داریم Design Pattern ها به 3 مدل دسته بندی میشن که به صورت خلاصه میشه:
الگوهای creational مسئله ی ایجاد شی رو بررسی می کنن. اینکه در هر شرایطی بهتره ایجاد شی رو به چه صورت انجام بدیم.
الگوهای Structural یا ساختاری روی مسئله ی ترکیب آبجکت ها و کلاس ها برای شکل دادن ساختار های بزرگ تر تمرکز دارن، به نحوی که قابلیت انعطاف و کارایی برنامه در حد امکان در بالاترین حد باشه.
الگوهای طراحی Behavioral یا رفتاری روی بحث تقسیم وظایف بین کلاس های مختلف تمرکز دارن.
بنابراین، الگوی Rules Engine به دسته Behavioral تعلق دارد چون شرطهای پیچیده ما را بین کلاسهای مختلف تقسیم میکند و رفتارهای متفاوت را چک میکند.
این الگو کجا به درد میخورد؟ به عنوان مثال، فرض کنید یک بانک میخواهد به یک فرد وام بدهد. بانک پارامترهای مختلفی را در نظر میگیرد: آیا فرد شغل دارد؟ میزان درآمد چقدر است؟ شغل فرد وابسته به چه ارگانی است؟ پس از بررسی و اعمال شرطها، در نهایت مشخص میشود که آیا فرد واجد شرایط وام هست یا نه.
یکی از مشکلاتی که این الگو برطرف میکند، مشکل "اصل باز/بسته" (Open/Closed Principle) است. به جای اینکه برای اضافه کردن یک Rule جدید کدهای قدیمی را تغییر دهید، میتوانید یک Rule جدید را اضافه کنید.
اما بدنه این pattern چیزی شبیه به تصویر زیر است
که یک سری شرط داریم که این شرط همون Rule های ما هستش که یک interface را پیاده سازی میکند و یک Engine داریم که لیستی از این 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; } }
این کد با شرطهای تو در تو پر شده است. حالا بهبودش میدهیم.
کد تمیز
اول، یک مدل تعریف میکنیم که همان 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 }; } }
حال، Engine خود را میسازیم:
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ها دارد و متدی که context (BmrModel) را میگیرد و مقادیر را محاسبه میکند.
سپس، کلاسی برای استفاده از این Engine مینویسیم:
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); } }
این کلاس با استفاده از Reflection، همه کلاسهایی که ICaloriesRule پیاده سازی کردهاند را پیدا کرده و آنها را به Engine ما میدهد.
برای تست، یک متد تست مینویسیم:
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); } }
الگوی Rules Engine مزایای متعددی دارد، از جمله امکان کار گروهی و افزایش قابلیت نگهداری کد. این الگو به ما اجازه میدهد که به سادگی Ruleهای جدیدی اضافه کنیم بدون اینکه نیاز به تغییر در کدهای قدیمی داشته باشیم. با استفاده از این الگو، میتوانیم پیچیدگیهای مربوط به شرطهای تو در تو را کاهش دهیم و کدی تمیز و ماژولار بنویسیم.
منابع و مراجع
برای مطالعه بیشتر در مورد Rules Engine Design Patterns، میتوانید به منابع زیر مراجعه کنید:
Design Patterns: Elements of Reusable Object-Oriented Software by Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides
Enterprise Integration Patterns: Designing, Building, and Deploying Messaging Solutions by Gregor Hohpe and Bobby Woolf
https://medium.com/
امیدوارم این مقاله برای شما مفید واقع شده باشد.