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)?