Refactoring If Statements to Lookup Tables

This is the first in a series of posts that explore refactoring if statements. In this one, I’ll cover refactoring if statements to lookup tables. These are if statements that you can also model as a key/value pair lookup. You may know lookups by another name such as dictionaries, hash sets, maps or object literals. This depends on which language you’re coming from but the concept is the same. Let’s dive in.

When refactoring code we sometimes come across if statements that look like this:

if(input == 1) return "Apples"
if(input == 2) return "Oranges"

This is just a mapping from an int to a string, a lookup of key/value pairs. We could also express the above as:

var fruits = new Dictionary<int, string>
{
    { 1, "Apples" },
    { 2, "Oranges" }
};

return fruits[input];

A couple of interesting things happen as a result of refactoring if statements to lookup tables. First, I think using a lookup is semantically truer to the nature of the code. You can use if statements to express the same thing as a dictionary, but why choose them when there’s a specific type we can use for key/value pairs? Second, you can make extracted lookups into configurable dependencies.

I’ve prepared some examples which we’ll look at here, you can find them over at https://github.com/Squaretechre/blog-example-code/tree/master/RefactoringIfStatements/RefactoringIfStatements.LookupTables.

Design Transformations Resulting From Refactoring If Statement to Lookup Tables

Below we have a little class that models a colour. It’s constructed with a hex colour code and has methods for obtaining the colour name and RGB values. If we have a look at the Name and Rgb methods though we can see the same pattern described above. In the Name method, the hex colour code is mapped to a colour name. Then in the Rgb method, a hex colour code is mapped to some RGB values.

public class Colour
{
    private readonly string _hex;

    public Colour(string hex)
    {
        _hex = hex;
    }

    public string Name()
    {
        if (_hex.Equals("#B22222")) return "firebrick";
        if (_hex.Equals("#FF6347")) return "tomato";
        if (_hex.Equals("#FFEFD5")) return "papayawhip";
        if (_hex.Equals("#7FFF00")) return "chartreuse";
        if (_hex.Equals("#F0FFF0")) return "honeydew";
        if (_hex.Equals("#F5FFFA")) return "mintcream";
        return string.Empty;
    }

    public RgbValues Rgb()
    {
        if (_hex.Equals("#B22222")) return new RgbValues(178, 34, 34);
        if (_hex.Equals("#FF6347")) return new RgbValues(255, 99, 71);
        if (_hex.Equals("#FFEFD5")) return new RgbValues(255, 239, 213);
        if (_hex.Equals("#7FFF00")) return new RgbValues(127, 255, 0);
        if (_hex.Equals("#F0FFF0")) return new RgbValues(240, 255, 240);
        if (_hex.Equals("#F5FFFA")) return new RgbValues(245, 255, 250);
        return RgbValues.Empty();
    }
}

It looks like we have a candidate for the lookup table transformation. Let’s try refactoring these if statements to a lookup table.

public class Colour
{
    private readonly string _hex;

    private readonly Dictionary<string, string> _names = new Dictionary<string, string>
    {
        { "#B22222",  "firebrick" },
        { "#FF6347",  "tomato" },
        { "#FFEFD5",  "papayawhip" },
        { "#7FFF00",  "chartreuse" },
        { "#F0FFF0",  "honeydew" },
        { "#F5FFFA",  "mintcream" },
    };

    private readonly Dictionary<string, RgbValues> _rgbValues = new Dictionary<string, RgbValues>
    {
        { "#B22222",  new RgbValues(178, 34, 34) },
        { "#FF6347",  new RgbValues(255, 99, 71) },
        { "#FFEFD5",  new RgbValues(255, 239, 213) },
        { "#7FFF00",  new RgbValues(127, 255, 0) },
        { "#F0FFF0",  new RgbValues(240, 255, 240) },
        { "#F5FFFA",  new RgbValues(245, 255, 250) },
    };

    public Colour(string hex)
    {
        _hex = hex;
    }

    public string Name()
    {
        return _names.ContainsKey(_hex) 
            ? _names[_hex] 
            : string.Empty;
    }

    public RgbValues Rgb()
    {
        return _rgbValues.ContainsKey(_hex)
            ? _rgbValues[_hex]
            : RgbValues.Empty();
    }
}

Applying the refactoring created two key/value lookups, one for the names and one for the RGB values. However, if we want the Colour class to support more colours in the future we’ll have to make the changes in two places. In this case, perhaps it isn’t the worst thing in the world. All the colour information is in one place and perhaps maintainers don’t add new colours that often. But in the name of design exploration let’s assume that we’re not happy with this and create a type that represents both the colour’s name and RGB values.

public class Colour
{
    private readonly ColourDescription _description;

    public Colour(string hex)
    {
        _description = ColourDescription.ForHex(hex);
    }

    public string Name()
    {
        return _description.Name;
    }

    public RgbValues Rgb()
    {
        return _description.Rgb;
    }
}


public class ColourDescription
{
    public string Hex { get; }
    public string Name { get; }
    public RgbValues Rgb { get; }

    private static readonly Dictionary<string, ColourDescription> Descriptions = new Dictionary<string, ColourDescription>
    {
        { "#B22222",  new ColourDescription("#B22222", "firebrick", new RgbValues(178, 34, 34)) },
        { "#FF6347",  new ColourDescription("#FF6347", "tomato", new RgbValues(255, 99, 71)) },
        { "#FFEFD5",  new ColourDescription("#FFEFD5", "papayawhip", new RgbValues(255, 239, 213)) },
        { "#7FFF00",  new ColourDescription("#7FFF00", "chartreuse", new RgbValues(127, 255, 0)) },
        { "#F0FFF0",  new ColourDescription("#F0FFF0", "honeydew", new RgbValues(240, 255, 240)) },
        { "#F5FFFA",  new ColourDescription("#F5FFFA", "mintcream", new RgbValues(245, 255, 250)) },
    };

    private static readonly ColourDescription Empty = new ColourDescription(string.Empty, string.Empty, RgbValues.Empty());

    private ColourDescription(string hex, string name, RgbValues rgb)
    {
        Hex = hex;
        Name = name;
        Rgb = rgb;
    }

    public static ColourDescription ForHex(string hex)
    {
        return Descriptions.ContainsKey(hex)
            ? Descriptions[hex]
            : Empty;
    }
}

The changes above introduce a ColourDescription type. This wraps a single dictionary with mappings from a hex colour code to a ColourDescription. It also has a static factory method to create a new instance for a hex code. But this has created two new problems. First, the original Colour class is starting to look like a Middle Man, it isn’t doing much at all. Second, the dictionary is starting to look a bit redundant. Each ColourDescription already has the hex colour code.

Maybe we can just delete the original Colour class, rename ColourDescription to Colour and change the dictionary to a list?

public class Colour
{
    public string Hex { get; }
    public string Name { get; }
    public RgbValues Rgb { get; }

    private static readonly List<Colour> Descriptions = new List<Colour>
    {
        new Colour("#B22222", "firebrick", new RgbValues(178, 34, 34)),
        new Colour("#FF6347", "tomato", new RgbValues(255, 99, 71)),
        new Colour("#FFEFD5", "papayawhip", new RgbValues(255, 239, 213)),
        new Colour("#7FFF00", "chartreuse", new RgbValues(127, 255, 0)),
        new Colour("#F0FFF0", "honeydew", new RgbValues(240, 255, 240)),
        new Colour("#F5FFFA", "mintcream", new RgbValues(245, 255, 250)),
    };

    private static readonly Colour Empty = new Colour(string.Empty, string.Empty, RgbValues.Empty());

    private Colour(string hex, string name, RgbValues rgb)
    {
        Hex = hex;
        Name = name;
        Rgb = rgb;
    }

    public static Colour ForHex(string hex)
    {
        return Descriptions.FirstOrDefault(x => x.Hex.Equals(hex)) ?? Empty;
    }

    public static Colour ForName(string name)
    {
        return Descriptions.FirstOrDefault(x => x.Name.Equals(name)) ?? Empty;
    }
}

The Colour class now looks a bit different from where it started out. A list has replaced the dictionary. It also has the “ForName” static factory method for creating instances by colour name. The implementation of “ForName” is easier to do with a list than a dictionary.

I think it’s interesting to see where transformations like refactoring if statements to lookup tables take a design. Sometimes applying a transformation like if statement to lookup can help you view the logic in a different way. This might then trigger ideas for other design changes and refactorings you could apply.

One thing to bear in mind is this example exists in a vacuum. Nothing was dependant on Colour except for the tests. It isn’t always easy to change the public interface of a class. When changing the public API you need to consider those who are dependent on it.

In this example, the change was safe enough. But if Colour did have multiple dependencies maybe ColourDescription should have kept the same interface as the original Colour class. Alternatively, the original Colour class could have hung around a little longer acting as an adapter. Who knows, while the constraints are away the mice will play. 🙃

Configurable Dependencies as a Result of Refactoring If Statement to Lookup Tables

The following example is something that isn’t a mapping of primitive to primitive. Below is a class that models some type of Order and it knows how to calculate a certain amount of discount for various customer statuses. We can see in the discount method that the if statements are doing a comparison on CustomerStatus and then performing some calculation involving the order total.

public class Order
{
    private readonly Money _total;
    private readonly Customer _customer;

    public Order(Money total, Customer customer)
    {
        _total = total;
        _customer = customer;
    }

    public Money Discount()
    {
        if (_customer.Status == CustomerStatus.Bronze)
        {
            return _total * 0.05m;
        }
        else if (_customer.Status == CustomerStatus.Silver)
        {
            return _total * 0.10m;
        }
        else if (_customer.Status == CustomerStatus.Gold)
        {
            return _total * 0.15m;
        }
        else if (_customer.Status == CustomerStatus.Platinum)
        {
            return _total * 0.20m;
        }

        return _total * 0;
    }
}

What we could do here is extract each discount calculation to its own function and create a map of CustomerStatus to Func<Money, Money>.

By applying this transformation we end up with a Dictionary<CustomerStatus, Func<Money, Money>>. This is interesting as it’s something we could now easily pull out of Order entirely.

public class Order
{
    private readonly Money _total;
    private readonly Customer _customer;

    private readonly Dictionary<CustomerStatus, Func<Money, Money>> _discounts = new Dictionary<CustomerStatus, Func<Money, Money>>
    {
        { CustomerStatus.General, (total) => total * 0.00m },
        { CustomerStatus.Bronze, (total) => total * 0.05m },
        { CustomerStatus.Silver, (total) => total * 0.10m },
        { CustomerStatus.Gold, (total) => total * 0.15m },
        { CustomerStatus.Platinum, (total) => total * 0.20m },
    };

    public Order(Money total, Customer customer)
    {
        _total = total;
        _customer = customer;
    }

    public Money Discount()
    {
        return _discounts[_customer.Status](_total);
    }
}

Below the dictionary has been extracted from Order and is now a configurable dependency. Again, this example lives in a vacuum. There’s no real reason for making the discounts configurable, I just needed something to demonstrate the technique.

public class Order
{
    private readonly Money _total;
    private readonly Customer _customer;
    private readonly Dictionary<CustomerStatus, Func<Money, Money>> _discounts;

    public Order(Money total, Customer customer, Dictionary<CustomerStatus, Func<Money, Money>> discounts)
    {
        _total = total;
        _customer = customer;
        _discounts = discounts;
    }

    public Money Discount()
    {
        return _discounts[_customer.Status](_total);
    }
}

public class OrderShould
{
    private readonly Dictionary<CustomerStatus, Func<Money, Money>> _discounts = new Dictionary<CustomerStatus, Func<Money, Money>>
    {
        { CustomerStatus.General, (total) => total * 0.00m },
        { CustomerStatus.Bronze, (total) => total * 0.05m },
        { CustomerStatus.Silver, (total) => total * 0.10m },
        { CustomerStatus.Gold, (total) => total * 0.15m },
        { CustomerStatus.Platinum, (total) => total * 0.20m },
    };

    [Fact]
    public void calculate_no_discount_for_general_customers()
    {
       var sut = new Order(Money.USD(100), Customer.General(), _discounts); 

       Assert.Equal(Money.USD(0), sut.Discount());
    }
}

Trivia Kata Example

Finally here’s an example from the popular Trivia legacy code kata. The Trivia kata is supposed to be similar to the game Trivial Pursuit. It goes something like this:

  1. The player makes a roll
  2. The number they roll is the number of places they advance on the board
  3. They receive a different category of question depending on how many places they advanced

I have included a subset of the original kata in the example code repository. It contains just enough code to make the lookup table refactoring work. Below is an extract from the repository, in it you’ll see the familiar lookup pattern. This time it’s in the CurrentCategory method and we’ve got a mapping of int to string.

public class Game
{
    // ...omitted for brevity

    private void AskQuestion()
    {
        if (CurrentCategory() == "Pop")
        {
            Console.WriteLine(_popQuestions.First());
            _popQuestions.RemoveFirst();
        }
        if (CurrentCategory() == "Science")
        {
            Console.WriteLine(_scienceQuestions.First());
            _scienceQuestions.RemoveFirst();
        }
        if (CurrentCategory() == "Sports")
        {
            Console.WriteLine(_sportsQuestions.First());
            _sportsQuestions.RemoveFirst();
        }
        if (CurrentCategory() == "Rock")
        {
            Console.WriteLine(_rockQuestions.First());
            _rockQuestions.RemoveFirst();
        }
    }

    private string CurrentCategory()
    {
        if (_places[_currentPlayer] == 0) return "Pop";
        if (_places[_currentPlayer] == 4) return "Pop";
        if (_places[_currentPlayer] == 8) return "Pop";
        if (_places[_currentPlayer] == 1) return "Science";
        if (_places[_currentPlayer] == 5) return "Science";
        if (_places[_currentPlayer] == 9) return "Science";
        if (_places[_currentPlayer] == 2) return "Sports";
        if (_places[_currentPlayer] == 6) return "Sports";
        if (_places[_currentPlayer] == 10) return "Sports";
        return "Rock";
    }
}

Like we’ve seen above, one way we could redesign this is to introduce a dictionary.

public class Game
{
    private const string CategoryPop = "Pop";
    private const string CategoryScience = "Science";
    private const string CategorySports = "Sports";
    private const string CategoryRock = "Rock";

    private readonly Dictionary<int, string> _categories = new Dictionary<int, string>
    {
        { 0, CategoryPop },
        { 4, CategoryPop },
        { 8, CategoryPop },
        { 1, CategoryScience },
        { 5, CategoryScience },
        { 9, CategoryScience },
        { 2, CategorySports },
        { 6, CategorySports },
        { 10, CategorySports },
        { 11, CategoryRock },
    };

    private void AskQuestion()
    {
        var placesAdvancedByCurrentPlayer = _places[_currentPlayer];

        if (CategoryFor(placesAdvancedByCurrentPlayer) == CategoryPop)
        {
            Console.WriteLine(_popQuestions.First());
            _popQuestions.RemoveFirst();
        }
        if (CategoryFor(placesAdvancedByCurrentPlayer) == CategoryScience)
        {
            Console.WriteLine(_scienceQuestions.First());
            _scienceQuestions.RemoveFirst();
        }
        if (CategoryFor(placesAdvancedByCurrentPlayer) == CategorySports)
        {
            Console.WriteLine(_sportsQuestions.First());
            _sportsQuestions.RemoveFirst();
        }
        if (CategoryFor(placesAdvancedByCurrentPlayer) == CategoryRock)
        {
            Console.WriteLine(_rockQuestions.First());
            _rockQuestions.RemoveFirst();
        }
    }

    private string CategoryFor(int placesAdvancedByCurrentPlayer)
    {
        return _categories[placesAdvancedByCurrentPlayer];
    }
}

I’d recommend having a go of the Trivia kata. Try refactoring if statements to lookup tables and see where it gets you! Thanks for reading.

Buy me a coffee Buy me a coffee

😍 ☕ Thank you! 👍

Share

Leave a Reply

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

Post comment