Reply to a reply... tweaking refactoring

This is a reply to Ben Alabaster's blog post, which is itself a reply. You can follow the trail yourself. I'll assume you've read the post - I'm not going to go over anything already written there, other than my comments.

I took issue with three aspects of Ben's refactoring:

  • The use of float for currency
  • The fact that "BaseRate" effectively doesn't have a well-defined unit; in some cases it's "dollars per hour" and in others it's "dollars per pay cheque, regardless of hours" smells
  • The use of properties for the pay functions

I'll tackle the last one first, because I was stupid. I suggested using public static readonly fields instead of properties. This was dumb. All we need is a simple static class with public static methods - we can use them with exactly the same source code as before for the Employee construction, but without all the fancy lambda expressions:

public static class PayCalculations
{
    public static float BasicWithoutOvertime(float hours, float baseRate)
    {
        return hours * baseRate;
    }

    public static float BasicWithOvertime(float hours, float baseRate)
    {
        if (hours < 40) return hours * baseRate;
        return ((hours - 40f) * 1.5f + 40f) * baseRate;
    }

    public static float Salary(float hours, float baseRate)
    {
        return baseRate;
    }

    public static float Contractor(float hours, float baseRate)
    {
        /* Base rate */
        float subtotal = Math.Min(hours, 40) * baseRate;
        hours -= Math.Min(hours, 40);
        /* Time plus a half */
        if (hours > 0) subtotal += 1.5f * Math.Min(hours, 20) * baseRate;
        hours -= Math.Min(hours, 20);
        /* Double time */
        if (hours > 0) subtotal += 2.0f * Math.Min(hours, 20) * baseRate;
        hours -= Math.Min(hours, 20);
        /* Double time plus a half */
        if (hours > 0) subtotal += 2.5f * hours * baseRate;

        return subtotal;
    }
}

Less code, less nesting, less use of fancy C# 3 features... generally nicer. The construction code remains the same, because it uses method group conversions to build the delegates.

Fixing the "float should be decimal" problem is easy, of course. Let's move on to the units and "wrong" values. The problem is that the BaseRate property means different things for different employees, and in some cases it's not even needed at all. That's a reasonably strong indicator that it's in the wrong place. Let's accept that all employees' pay may depend on the number of hours they've worked that week, but that's all. Everything else depends on the particular contract that the employee is using, and that can vary. So let's put the variance into what creates the function - so we can build a "salaried employee on $2935 week" function, a "per hour worker on $40.25 basic without overtime" etc. This is effectively like creating an IPayContract interface and multiple implementations, then creating instances of those implementations which have specific values. Except we're using delegates... so having ripped out the lambda expressions, I'm going to put them back in :) But this time we're just going to use a Func<decimal, decimal> as we only to know how much to pay given a certain number of hours worked. (The first decimal here could potentially be float or double instead, but if anyone ever did claim to work 3.1 hours, they'd probably want pay that reflected it.)

Here are the pay calculations:

public static class PayCalculations
{
    public static Func<decimal, decimal> BasicWithoutOvertime(decimal dollarsPerHour)
    {
        return hours => dollarsPerHour * hours;
    }

    public static Func<decimal, decimal> BasicWithOvertime(decimal dollarsPerHour)
    {
        // Use an alternative approach just for LOLs
        return hours => {
            decimal basicHours = Math.Min(hours, 40);
            decimal overtimeHours = Math.Max(hours - 40, 0);
            return (basicHours * dollarsPerHour) + (overtimeHours * dollarsPerHour * 1.5m);
        };
    }

    public static Func<decimal, decimal> Salary(decimal dollarsPerWeek)
    {
        // This *looks* like the units are wrong... but it's okay, see text.
        return hours => dollarsPerWeek;
    }

    public static Func<decimal, decimal> Contractor(decimal baseRate)
    {
        return hours => {
            // 0-40 hours
            decimal basicHours = Math.Min(hours, 40);
            // 40-60 hours
            decimal overtime = Math.Min(Math.Max(hours - 40, 0), 20);
            // 60-80 hours
            decimal doubleTime = Math.Min(Math.Max(hours - 60, 0), 20);
            // 80+ hours
            decimal chargingThroughTheNoseTime = Math.Max(hours - 80, 0);

            return (basicHours * baseRate)
                 + (overtime * baseRate * 1.5m)
                 + (doubleTime * baseRate * 2m)
                 + (chargingThroughTheNoseTime * baseRate * 2.5m);
        };
    }
}

And now, when we construct the employees, we don't have to specify a base rate which was only meaningful in some cases - instead, we give that value to the pay calculator instead:

List<Employee> employees = new List<Employee>
{
    new Employee("John", "MacIntyre", PayCalculations.BasicWithoutOvertime(40.25m)),
    new Employee("Ben", "Alabaster", PayCalculations.BasicWithOvertime(40.25m)),
    new Employee("Cory", "Fowler", PayCalculations.Salary(2935m)),
    new Employee("John", "Doe", PayCalculations.Contractor(150m)),
    new Employee("Jane", "Doe", hours => 3500m),
    new Employee("Joe", "Bloggs", hours => 34.25m * Math.Max(hours, 15))
};

Now, look at the Salary method and the comment in it... I'm still concerned about the units. We're meant to be returning a simple dollar value (and in another system I'd probably bake that information into the types used) but we've still got dollarsPerWeek. What's wrong here? Well, it all boils down to an assumption: we're running this once a week. We've got a constant of 1 week... so we could rewrite the method like this:

public static Func<decimal, decimal> Salary(decimal dollarsPerWeek)
{
    decimal weeksWorked = 1m; // We run payroll once per week
    return hours => dollarsPerWeek * weeksWorked;
}

Now the units work... although it looks a bit silly. Of course, it makes our assumption very explicit - and easy to challenge. Maybe we actually run payroll once per month... in which case the fact that we're expressing the salary in dollars per week is wrong - but very obviously wrong, which is a good thing.

Conclusion

It doesn't feel right to have a blog post with no conclusion. Lessons applied here:

  • Remember all the tools available, not just the shiny ones. Using method group conversions made the initial "constant function" approach simpler to understand.
  • Units are important! If the same field effectively represents different units for different instances, there's something wrong
  • If a field is only relevant for some instances of a type, it's probably in the wrong place
  • Don't use floats for currency. See any number of Stack Overflow questions for reasons why :)

EDIT: As noted by Barry Dorrans, there's a lot of scope for introducing constants in here, for further goodness. I'm not going to bother updating the code just for Barry though. That way madness lies.

Published Wed, Sep 15 2010 20:29 by skeet
Filed under:

Comments

# re: Reply to a reply... tweaking refactoring

Hi Jon,

I addressed the use of floats in the comments of my blog. .NET itself is optimized to use the double struct, which is a good choice, except decimal plays better with financial calculations.

Units are definitely important, that is something that was over looked in all of our posts. I'm glad you pointed it out :)

Thanks!

Cory "SyntaxC4" Fowler

Wednesday, September 15, 2010 2:45 PM by Cory Fowler

# Its using floats because the original code was using floats

Hi Jon,

Thanks for the well thought out response.

Just an FYI - the reason the code uses floats is because the original code uses floats.  I didn't like them either, but didn't feel it was appropriate to change them.

Regards,

John

Wednesday, September 15, 2010 4:44 PM by John MacIntyre

# re: Reply to a reply... tweaking refactoring

Hi Jon. I like your refactorings.

I wonder, though, whether delegates are appropriate at all for this kind of problem. To put it in a real-world context, suppose a new requirement emerged later that some people should get paid time-and-a-half for Saturdays, and double-time for Sundays.

Would you be OK with the amount of effort required to refactor the delegate-based design? And how comfortable would you be with Func<decimal,decimal,decimal,decimal>?

Joe

Wednesday, September 15, 2010 9:49 PM by Joe Albahari

# re: Reply to a reply... tweaking refactoring

@Joe: At that point the problem isn't that we're using delegates - it's that our input model is inappropriate. I'd be happy refactoring to a Func<TimeSheet, decimal> for example :)

Thursday, September 16, 2010 12:27 AM by skeet

# re: Reply to a reply... tweaking refactoring

Hi John,

Let me add my five cents why I prefer your solution to the others.

Reading all the related posts and replies makes me feel that refactoring should be handled with more care. The original posts introducing inheritance give an elegant solution; however they neglect that inheritance is a very powerful abstraction that has huge costs. For developers it can be a pain to maintain or even to understand large class hierarchies. Too much of inheritance can also lead to corrupted solutions when developers tend to overuse inheritance unnecessary. All in all I think the original code might be ugly though simple to understand and maintain. If there is a grow in complexity of wage calculations I would choose your solution since it is compact enough to allow fast and clean changes. Not that the other solutions are bad just they are too much for a "refactoring over a boolean". My experiences shows that there are less problems with not-that-shiny but simple code compared to brilliantly structured but eye-blowing code.

Cheers,

Gergo

Thursday, September 16, 2010 4:14 AM by Gergo Buchholcz

# re: Reply to a reply... tweaking refactoring

I *really* agree with your assessment that you need to have the correct units for everything, but I don't see why you stopped where you did.

I'd go a bit further and say that yes, don't use floats for currency, but using decimal is also incorrect. A $100 bill doesn't just contain the information '100'. It is also of the unit 'dollar'.

I often create a small immutable class for representing money of all kinds - even if the system is single currency only. It tends to help the other code keep its dirty hands off the monetary values (i.e. multiplying 100 dollars with another 100 dollars). And it's so much easier to put the 'must be > 0' checks in the money class instead of in the validation code for every method accepting a decimal.

Thursday, September 16, 2010 11:01 AM by Joachim Jungner

# re: Reply to a reply... tweaking refactoring

@Joachim: Agreed - as I gave a brief nod to in the article ("and in another system I'd probably bake that information into the types used"). I suspect F# Units of Measure would help here, but I haven't looked at them for a while.

The main reason for not doing it here was due to time considerations.

Thursday, September 16, 2010 11:12 AM by skeet

# re: Reply to a reply... tweaking refactoring

Ahh.. Missed that. Sorry. I'm glad you agree, though.

Thursday, September 16, 2010 11:40 AM by Joachim Jungner

# re: Reply to a reply... tweaking refactoring

The right choice of the logic for calculating the salary is determined by the "Employee type".  Where is this (If "type=regular employee" then "use this logic for the salary calculation") reflected (use factories?) ?  

Thursday, September 16, 2010 11:03 PM by Magesh