How to Use the “Rules Design Pattern” With An Example

If the rules design pattern is new to you, let me give you a quick run down of what it is and how it works.

The rules design pattern is useful for situations where there are many different options and the “best” option needs to be selected. It is also useful if you find yourself getting into if/else hell. Usually, these options start off as something simple, and continue to grow in complexity over time. One easy example for this is would be discounts in a shopping cart application. It often has code like the example below:

decimal discount = 0;

//5% off because the user was referred
if (user.wasReferred)
{
    discount = .05m;
}

//10% off for seniors
if (user.age >= 55)
{
    discount = .10m;
}

if(user.IsMember)
{
    if(user.FirstPurchase)
    {
        discount = .25m;
    }
    else
    {
        discount = .12m;
    }
}

As you can see in the code above, not only does the order of the rules matter, but adding a new rule may cause more if/else statements to be written to keep things straight. Adding just a few more rules will greatly increase the complexity and maintainability of this code.

The solution to this is to create an interface and make each rule a class that inherits from that interface.

 interface IRule
 {
    decimal GetDiscount(User user);
 }

From there you can calculate each rule independently of each other and take the best discount.

public class MemberDiscount : IRule
{
    public decimal GetDiscount(User user)
    {
        return user.IsMember ? .12m : 0;
    }
}
public class MemberDiscount : IRule
{
    public decimal GetDiscount(User user)
    {
        return user.IsMember ? .12m : 0;
    }
}
public class MemberFirstPurchaseDiscount : IRule
{
    public decimal GetDiscount(User user)
    {
        return user.IsMember && user.IsFirstPurchase ? .25m : 0;
    }
}
public class ReferredDiscount : IRule
{
    public decimal GetDiscount(User user)
    {
        return user.WasReferred ? .05m : 0;
    }
}
public class SeniorDiscount : IRule
{
    public decimal GetDiscount(User user)
    {
        return user.Age >= 55 ? .10m : 0;
    }
}

Then you just need to add all of the rules into a collection and get the best option of the list.

 List<IRule> rules = new List<IRule>()
            {
                new MemberDiscount(),
                new MemberFirstPurchaseDiscount(),
                new ReferredDiscount(),
                new SeniorDiscount()
            };

            decimal discount = rules.Select(rule=>rule.GetDiscount(user))
                                    .Max();

As you can see, it is much easier to add/remove rules as the business logic changes. Also because of the changes that we made to reduce the cross dependency between the rules and removing the dependency on the ordering of the rules it is much easier to maintain and change in the future.

Here is link to the source if you want to take a look at it https://bitbucket.org/AndySattler/rulesdesignpattern/src/master/