One of my clients has recently asked me to implement a Wage Calculator.
Calculating the wage of an employee could be really complicated and was likely to change quite often. And on top of that, I was given 5 days to implement the entire backend.
A number of architectural guidelines ran through my head as I constructed the mental model for this new application. How should I implement this? Maybe a configurable rule-engine? Too much work, too little time. Calculations stored in the database? Too complex for team-members to maintain. I decided to use a direct and purely SOLID approach.
If you don't know SOLID, I suggest you start researching it right away. I wrote this blog-post to provide a meaningful implementation and I will not be explaining the principles themselves.
Most of the existing wage calculators in the client’s portfolio have one calculation method with a lot of if / switch / case statements. This, off course, is never a good idea. It breaks the single responsibility principle and the open/closed principle. And since my code needed to allow quick and safe modifications, I decided not to reuse the large method.
This is how I implemented the Wage Calculator:
We are going to split each different calculation in a different processor class. Each class will have a way of identifying itself and a way to do the actual calculation. This requires an interface:
public interface IWageProcessor
{
// Is this processor a match
bool IsMatch(WageInfo oldWageInfo);
//calculate the wage
void Calculate(WageInfo newWageInfo, WageInfo oldWageInfo, decimal numberOfHoursPerWeek);
}
The 'IsMatch' function will return true if certain conditions are met. The 'CalculateWage' method can then be called to perform the actual calculation.
Now that we have the interface we’ll need some kind of way to register the different kinds of processors and identify the right one. This will be the actual wage calculator:
public static class WageCalculator
{
private static readonly List _WageProcessors;
// Calculates the wage
public static void CalculateWage(eWageInfo newWageInfo, WageInfo oldWageInfo, decimal numberOfHoursPerWeek)
{
//fetch the processor that matches the current case
var singleOrDefault = _WageProcessors.SingleOrDefault(c => c.IsMatch(oldWageInfo));
if (singleOrDefault != null)
{
//if we found a processor, calculate the result
singleOrDefault.Calculate(newWageInfo, oldWageInfo, numberOfHoursPerWeek);
}
else
{
//otherwise add an error to the line
newWageInfo.ErrorMessage = "No valid processor found..";
}
}
}
This class has a list of IWageProcessor implementations and one 'Calculate' method. In this method we invoke the IsMatch of each registered IWageProcessor implementation and invoke it’s 'CalculateWage' method if a match has been found.
Nothing extremely difficult there. Let’s take a look at a couple of WageProcessor implementations:
public class NoGrossWageNoScaleGrossWageProcessor : IWageProcessor
{
public bool IsMatch(eWageInfo oldWageInfo)
{
return oldWageInfo.GrossWage == 0 && oldWageInfo.ScaleGrossWage == 0;
}
public void Calculate(eWageInfo newWageInfo, eWageInfo oldWageInfo, decimal numberOfHoursPerWeek)
{
newWageInfo.ScaleGrossWage += newWageInfo.AdditionFullTime;
newWageInfo.GrossWage = newWageInfo.ScaleGrossWage;
}
}
public class ScaleGrossWageWithAdditionNotPartTimeProcessor : IWageProcessor
{
public bool IsMatch(eWageInfo oldWageInfo)
{
return oldWageInfo.ScaleGrossWage != 0 && oldWageInfo.AdditionFullTime != 0 && oldWageInfo.PartTime == false;
}
public void Calculate(eWageInfo newWageInfo, eWageInfo oldWageInfo, decimal numberOfHoursPerWeek)
{
newWageInfo.GrossWage = newWageInfo.AdditionFullTime + newWageInfo.ScaleGrossWage;
}
}
After we register them in the WageCalculator’s constructor -don't hesitate to use dependency injection- we have a working SOLID implementation of a wage calculator.
We can simply swap out modules for different implementations. The wage calculator is loosely coupled, open for extension and closed for modification.
Each module is responsible for its own calculations, they each have a single responsibility.
In a nutshell, this implementation is easy to maintain and allows future modifications without the risk of breaking existing code. It’s relatively simple, it doesn’t take a rocket scientist to debug or understand. And, it allows you to easily unit and integration test your application. You can choose to test each processor independently or test the wage calculator as a whole.
The downside is that it adds complexity to the project. It's also harder to debug. There ain't no such thing as a free lunch.
But given the advantages, I think this is the way to go.
Comments?
Leave us your opinion.