Modeling business rules

In this post, I want to discuss a pattern that I have seen emerging emerging so often, that I think it is a structural concept. The pattern is about a common technique to express business rules, irrespective of domain.

In the previous post, I wrote about a technique to decouple cross-cutting concerns from business code using the Decorator pattern. This post will discuss another usage of the Decorator pattern so that business rules can be expressed in a common format.

The problem

The best way to introduce this pattern is through an example. Let’s imagine a web shop checkout module, where discounts are calculated. There are many possible ways to define a discount:

  • Based on the contents of the shopping cart: special items may come at a discount, or combinations of items, or when a certain quantity of items is selected
  • A discount may be added for first-time customers
  • Or customer loyalty discounts could be included
  • Or the web shop might want to apply discounts to customers from certain regions
  • Discounts may also be given based on the amount of inventory in stock, or based on customer interest.
  • Discounts may be given for a specific period in time.
  • And rules may be mixed or not, according to what the web shop owner wants to support.

Those business rules are all very different, and they all require completely different input data. Some rules require information from the customer’s account, other rules require geographical data or inventory information.

If the discount calculation were implemented in a single class, then this class would be very complex, and have a large number of dependencies. Since it is all about business rules, the logic in the class may change frequently. It might become a bottleneck in the application development. So you want to decouple the business rules, and be able to combine them as needed. That is the flexibility that is ultimately required.

Decoupling business rules

In order to decouple and flexibly mix rules, we need to mold the business rules in a uniform interface. Once we do that, we have separated the mixing of business rules of the business rules themselves. Something like the following IDiscountCalculator abstracts away all implementation details.

public interface IDiscountCalculator {
    public decimal Calculate();
}

This interface declaration abstracts away all implementation details. In other words, the interface does not say anything about a particular rule; it only makes sure that a rule can be applied in the same way. For example, here are possible implementations for the rules to apply discounts for first-time customers and special items:

public class FirstTimeCustomer : IDiscountCalculator {
    private Account _account;
    private decimal _discount;
    
    public FirstTimeCustomer(Account account, decimal discount) {
        _account = account;
        _discount = discount
    }
    
    public decimal Calculate() {
        return _account.IsFirstTimeCustomer()
            ? _discount
            : 0.0m;
    }
}


public class SpecialItemDiscount : IDiscountCalculator {
    private Item _item;
    private Cart _cart;
    private decimal _discount;
    
    public SpecialItemDiscount(Cart dart, Item item, decimal discount) {
        _cart = cart;
        _item = item;
        _discount = discount;
    }
    
    public decimal Calculate() {
        return _cart.Contains(_item)
            ? _discount
            : 0.0m;
    }
}

As you see from the above implementations, they operate on completely different data, yet they implement the same interface.

With these sample implementations, it is now possible to define a single rule in the web shop. But a good web shop has often multiple kinds of discounts to offer.

So how can we combine multiple rules? That depends on what the shop owner wants, so it must be possible to combine rules in multiple ways. For example, when only the rule with the maximum discount must be applied:

public class ApplyHighestDiscount : IDiscountCalculator {
    private IDiscountCalculator[] _inner;
    
    public ApplyHighestDiscount(params IDiscountCalculator[] inner) {
        _inner = inner;
    }
    
    public decimal Calculate() {
        return _inner
            .Select(calculator => calculator.Calculate())
            .Append(0.0m)
            .Max();
    }
}

Or, when a some rules must be applied regardless of other rules:

public class SummedDiscount : IDiscountCalculator {
    private IDiscountCalculator[] _inner;
    
    public SummedDiscount(params IDiscountCalculator[] inner) {
        _inner = inner;
    }
    
    public decimal Calculate() {
        return _inner
            .Select(calculator => calculator.Calculate())
            .Sum();
    }
}

As you see, a combination of rules can be expressed as a IDiscountCalculator of itself. This makes more complex combinations of rules possible. In case a shop owner wants to apply the first-time customer regardless of other discounts, this can be defined as something like:

new SummedDiscount(
    new FirstTimeCustomer(account, 5.0m),
    new ApplyHighestDiscount(
        new SpecialItemDiscount(...),
        new SpecialItemDiscount(...)));

There you have it: business rules can be defined any way you like, and additionally can be mixed and match according to business needs.

Towards a pattern

The above is just a contrived example, but it states the power of this design. It is not just applicable to discount calculations, but it can be applied in any situation where diverse business rules need to be combined.

The general approach is a two-step process:

  1. Define a role interface with the required methods, and implement the basic business rules according to this role interface
  2. Define ‘meta’ rules to combine basic rules.

This approach and the resulting pattern can be applied regardless of the domain or the specific business rules to be implemented, so it can be used in completely different contexts.

To give a completely different example: you might be able to express your code as a bunch of ICommand implementations, where the ICommand interface consists of only an Execute() method. The sole fact that you can express things using this interface makes it possible to combine basic commands. You can have an ICommand implementation that runs commands in parallel, or another that runs it sequentially, or another that logs the start and end of an inner command or many more things that you can think of:

public class ParallelCommand {
    private readonly ICommand[] _inner;
    
    public ParallelCommand(params ICommand[] inner) {
        _inner = inner;
    }
    
    public void Execute() {
        Parallel.ForEach(_inner, new ParallelOptions(), cmd => cmd.Execute());
    }
}

public class ConditionalCommand {
    private readonly Func<bool> _condition;
    private readonly ICommand _inner;
    
    public ConditionalCommand(Func<bool> condition, ICommand inner) {
        _condition = condition;
        _inner = inner;
    }
    
    public void Execute() {
        if(_condition()) 
            _inner.Execute();
    }
}

Criticism

When developing software and refactoring pieces of code, I hit this pattern several times without recognizing that a structural principle was going on. Sometimes coworkers where skeptical about the resulting design. Their criticism mostly boiled down to two points that I don’t want to leave unattended in this post.

The first point is that the pattern results in a “large representational gap”: the discrepancy between the actual code and what it represents would be too large, and not applying this pattern would be better express what is going on.

But this argument could be given any time a design pattern is applied. Every design pattern has an associated cost, because it introduces additional software layers. Still, design patterns are useful because they make code maintainable. When design patterns are known to other developers, then a possible representational gap is much less of an issue. So I think that the cause for this criticism is more related to lack of experience with this pattern than with anything else.

Another criticism is that this scheme results in a new language. You cannot just do 1 + 1, but instead must create a new AdditionExpression(new Constant(1), new Constant(1)). In some cases this may be valid criticism; a new language has a cost and the question is whether this is worth it. However, when expressing business rules like in the previous examples, the introduction of a new language can be considered fact of life. After all, we’re expressing business rules in some business domain, so a new language to make these expressions possible might be perfectly valid. A language like C# is too general to express these rules for us, so we must make our own tools here. For an object-oriented language, it is useful to model business rules and combinations of rules as classes; this makes it easy to adhere to the Single Responsibility Principle.

Leave a Reply

Your email address will not be published. Required fields are marked *