-2

I'm currently trying to solve a problem with some legacy code that makes some calculations in order to find out the final value of a monetary benefit. The legacy code uses an imperative approach with lots of if and elses to handle calculation rules for each kind of benefit, which I believe will be hard to mantain and reuse, since those rules can change drastically based on a change of law. (not to mention that the calculation logic is heavily tangled with presentation logic)

So I've been trying to find a design pattern which could help me in this situation. My initial thought was to use the Strategy pattern to handle the different kinds of calculations and a factory to choose the correct strategy implementation, but I believe this wont work out due to the number of different calculations (there's 15 currently, with more to be defined).

So after further research, I've found out about the Rule Pattern and the Specification Pattern and I thought they looked promising so I tried to implement a solution by adapting them, but I've hit some roadbumps.

My implementation basically tries to select the appropriate calculation rules using the Specification pattern, and then apply the appropriate calculation rules in the order they're defined. Each specification would have a pipeline of calculation rules attached to them, and if the specification is suitable, the pipeline of rules would be applied.

Here's my implementation:

The specification would have a set of rules, with a method IsSatisfiedBy defining if the rule should be applied base on the benefit data and the calculation rules, which are registered by an domain expert.

public abstract class BenefitSpecification
{
    protected ICollection<IBenefitRule> Rules { get; set; } = new List<IBenefitRule>();

    public abstract bool IsSatisfiedBy(BenefitData benefitData, CalculationRule benefitRule);

    public abstract void CreateRuleSet(BenefitData benefitData);

    public decimal ApplyTo(BenefitData benefitData)
    {
        decimal total = 0M;
        foreach (var rule in Rules)
        {
            total = rule.ApplyRule(total);
        }
        return total;
    }
}

An hypothetical example of an concrete specification would be:

public class IntegralCalculation
{
    public override bool IsSatisfiedBy(BenefitData data, CalculationRule rule) => rule.IsIntegral;
    public override void CreateRuleSet(BenefitData data)
    {
        Rules.Add(
           new SumContributions(data.Contributions),
           new ApplyTax(),
           new MultiplyBy(2),
           new LimitBy(data.BenefitLimit)
        );
    }
}

The benefit rules would be simple mathematical operations:

public interface IBenefitRule
{
    decimal ApplyRule(decimal value);
}

I'm not sure if I'm overcomplicating stuff, but the example demonstrated here is a simplified version of the real rules, which have more logic inside them. The reason why I'm trying to do it this way is because I want want to reuse calculation logic in other specifications, and sometimes change the order they're applied based on the benefit data.

The roadbump that I've hit is that some necessary information is not available on the BenefitData alone and to get them I would have to break the interface. I thought about registering the benefit rule in the DI Container and accessing the database to get the data, but something about this approach doesn't feel right.

An example of the problem would be:

public class ReajustContributions : IBenefitRule
{
    /** properties defined  here **/

    /**
     * the reajustIndexes are not available in the BenefitData, so I would
     * have to query the database somehow.
     */
    public ReajustContributions(
       IEnumerable<Contributions> contributions,
       IEnumerable<ReajustIndex> reajustIndexes
    )
    {
       _contributions = contributions;
       _reajustIndexes = reajustIndexes;
    }

    public decimal ApplyRule(decimal value)
    {
       return /** reajusted values **/
    }
}

So my question is Is there a better or simpler design pattern to solve this kind of problem (selection of calculation rules based on business rules)?

2
  • Does this answer your question? Choosing the right Design Pattern
    – gnat
    Commented Apr 27, 2021 at 17:29
  • More or less, I was trying to find some pattern which could implement a rudimentary version of the "definition under conditions" pattern that is described in the Catala readme, so the question is bit more specific. I do think that I'm trying to use patterns in a place where they're not applicable, but I don't know what else to do. Commented Apr 27, 2021 at 18:05

2 Answers 2

3

Unless there is a generic scheme that all possible rules fit, then there is no pattern you can apply.

So. If for example you have a bunch of conditions, all of which have to be true in order to qualify for a benefit, then yes you can use something like the rules pattern, or validation logic. Where you can specify new conditions and add them to a list.

If all the conditions apply some sort of multiplier to a final number then yes you can multiply all the trues together etc.

But! if the rules can do pretty much anything, then any system you invent is tending towards a generic programming language. One of which you have readily to hand. Don't invent a new one!

Big conditional blocks and imperative programming get a bad rep, but everyone understands an if statement and a for loop. For this kind of thing they can be the best approach.

Of course separate it out from the rest of the logic, use private methods to encapsulate calculations and complex boolean statements etc. But don't try to be too clever, next year some new law will come along and muck up your system.

1
  • Yeah, I was wondering if what I was trying to represent was too broad for a pattern (because I couldn't find the pattern). What you said about it becoming a general programming languague is very true, in fact there's one language called Catala that was created specifically for this problem, unfortunately it's not mature yet. So I'll have to find other ways. Thanks for the guidance. Commented Apr 27, 2021 at 16:40
1

In order to solve the "Additional data" issue, I think you can make things a little bit dirty and just share sort of "AdditionalDataRegistry" or a "SharedDataRegistry". This registry will contain or will proxy to any repo needed to get additional data requested by rules. It will be injected in every rule that can have read-only access to that shared data: obviously can have access to data that is not relevant to the rule itself but if it's exposed as read-only I don't see any problem. In this way, you can avoid handling all the possible dependency permutations for each rule.

Another thing you can do is to use some scripting language (Python, Lua, Javascript) to implement strategies for these calculations and store these scripts on a database. They will be evaluated at runtime by a Python/Lua/Javascript engine hosted on your application. The main benefit is that you can add and modify strategies at runtime without any new deployment. The cons are that those scripts are more difficult to test and you have to find a good way to expose data to the scripting language engine.

Need to say that it should be considered only if it really worth the effort and a need of dynamic rules changes are really part of your business and not something happening due to bad organization or bad project management.

1
  • I like the suggestion of the registry, but I'll continue with the imperative approach for a while until I see the pattern emerge (If there's a pattern) and If I get more confident, I'll see if I can make it work. The dynamic changes of the rules is a side effect of the way the pertinent laws are defined, which has different rules depending on the context data. Even thought the law doesn't change very often, there are lots of selection rules, which leads to lots of if and elses. Commented Apr 27, 2021 at 22:51

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.