DEV Community 👩‍💻👨‍💻

Karim Fahmy
Karim Fahmy

Posted on

System Design| Business Rules Implementation


let's assume having to support some legacy code that has a lot of conditional logic and poor quality.

as well it's ongoing development on the system so It can be quite hard to integrate new rules as the code can be difficult to understand and to digest what is going on.

This sort of code often has comments explaining what the different pieces of conditional logic are doing.

The problems only gets worse as you have to add more conditions over time.

You can get a better understanding of how the Rules Pattern works by checking out the code in my github repo.

the example we have about e-commerce store that has discount criteria feature

public class DiscountCalculator
{
    public decimal CalculateDiscountPercentage(Customer customer)
    {
        decimal discount = 0;
        if (customer.DateOfBirth < DateTime.Now.AddYears(-65))
        {
            // senior discount 5%
            discount = .05m;
        }

        if (customer.DateOfBirth.Day == DateTime.Today.Day &&
            customer.DateOfBirth.Month == DateTime.Today.Month)
        {
            // birthday 10%
            discount = Math.Max(discount, .10m);
        }

        if (customer.DateOfFirstPurchase.HasValue)
        {
            if (customer.DateOfFirstPurchase.Value < DateTime.Now.AddYears(-1))
            {
                // after 1 year, loyal customers get 10%
                discount = Math.Max(discount, .10m);
                if (customer.DateOfFirstPurchase.Value < DateTime.Now.AddYears(-5))
                {
                    // after 5 years, 12%
                    discount = Math.Max(discount, .12m);
                    if (customer.DateOfFirstPurchase.Value < DateTime.Now.AddYears(-10))
                    {
                        // after 10 years, 20%
                        discount = Math.Max(discount, .2m);
                    }
                }

                if (customer.DateOfBirth.Day == DateTime.Today.Day &&
                    customer.DateOfBirth.Month == DateTime.Today.Month)
                {
                    // birthday additional 10%
                    discount += .10m;
                }
            }
        }
        else
        {
            // first time purchase discount of 15%
            discount = Math.Max(discount, .15m);
        }
        return discount;
    }
}
Enter fullscreen mode Exit fullscreen mode

The Rules Design Pattern
the fact that this complexity will increase as more rules are added.

I was looking around for a design pattern that might address those things and came across the Rules Pattern

The Rules Pattern works by separating out the rules from the rules processing logic applying the Single Responsibility Principle.

This makes it easy to add new rules without changing the rest of the system applying the Open/Closed Principle.

this image of new design

code map

illustration of the above image:

  • IRule : contains all methods of rule flow such ClearConditions() , Initialize() , IsValid() , Apply() , OnRuleViolated() as well you can add OnRuleCompleted()

  • IRule implemented by abstract class BaseRule<T> that use dependency of ICondition that has expression of rule requirement

  • RulesEngine.cs has ApplyRule() which the execution path of rule

  • the rules classes itself inherit base rule and override the method body

BirthdayDiscountRule example:

public class BirthdayDiscountRule : BaseRule<Customer>
    {
        public override void Initialize(Customer obj)
        {
            Conditions.Add(new ConditionExtension(CustomerExtensions.IsBirthday(obj)));
        }

        public override Customer Apply(Customer obj)
        {
            obj.Discount += .10m;
            return obj;
        }
        public override void OnRuleViolated(Customer obj)
        {
            Console.WriteLine($"{this.GetType().Name}  Violated");
        }
    }
Enter fullscreen mode Exit fullscreen mode

this code snipped of final result

class Program
    {
        static void Main(string[] args)
        {
            var productA = new Product { Price = 100, Name = "Apple Watch" };
            var productB = new Product { Price = 50, Name = "Samsung Watch" };

            var customer = new Customer
            {
                Products = new List<Product> { productA, productB, productA, productA },
                DateOfFirstPurchase = DateTime.Now.AddYears(-2),
                DateOfBirth = DateTime.Now.AddYears(-5),
            };
            customer.TotalValue = customer.Products.Sum(p => p.Price);

            var ruleManager = new CustomerRuleManager(customer);
            var result = ruleManager.Run();
        }
    }
    public class CustomerRuleManager
    {
        private readonly Customer _customer;

        public CustomerRuleManager(Customer customer)
        {
            _customer = customer;
        }

        public Customer Run()
        {
            _customer
                .ApplyRule(new BirthdayDiscountRule())
                .ApplyRule(new BuyThreeGetThirdFree("Apple Watch"))
                .ApplyRule(new FreeShipping())
                .ApplyRule(new LoyalCustomerRule(5, 0.12m))
                .ApplyRule(new NewCustomerRule())
                .ApplyRule(new RetiredDiscountRule());

            return _customer;
        }
    }
Enter fullscreen mode Exit fullscreen mode

Top comments (0)

🌚 Life is too short to browse without dark mode