Real-World Refactoring: Switch Statement to a Class Hierarchy

When I see a switch statement in C#, I kind of cringe. In this post, I'll demonstrate how to refactor a switch statement to a better, more maintainable class.

Written by Jonathan "JD" Danylko • Last Updated: • Develop •
Switch Statement refactoring

While a switch statement can help in the heat of coding, there are times when it can be a code smell.

When you see a condition or state that contains more function than state, that could be a sign that you need to move it out to a class making it more maintainable.

Code Smells are pieces of code that were written in a hap-hazard way to get the desired functionality, but contains hard to read code. One such book that touches on these particular topics is the Refactoring: Improving the Design of Existing Code by Martin Fowler and Kent Beck (I talk about another book that this technique is based off of in the conclusion below).

This post is geared more towards the obvious use of a switch statement that could be refactored out to a more reusable class. So once you see a switch statement, you could almost immediately check to see if the code SHOULD be refactored into a class.

Let's demonstrate with an example.

A Possible Scenario

Let's say you are a international consultant who helps clean up code and you travel near and far across the globe.

A client asks you to work on their ecommerce website and wants you to add a Christmas List function to their site.

As you look over their site, you notice they have search functionality. That search returns a list of products or services to the consumer.

But you notice another area on the site that contains Favorites, another type of list that returns a number of products.

Finally, you notice still another area called Wish List, which is (take a guess) another type of product list.

So they need to add another list called Christmas List. This list will be a shopping list of (yep!) products to help you make your decision of what to get for everyone.

You think to yourself, "It should be easy to add this Christmas Product List to their code."

You open up the code and you see the following:

public enum ListType
{
    SearchResults,
    Favorites,
    WishList
}
public class ProductList
{
    public IEnumerable<Product> GetResults(ListType type)
    {
        IEnumerable<Product> results = null;
        switch (type)
        {
            case ListType.SearchResults:
                results = GetSearchResults();
                break;
            case ListType.Favorites:
                results = GetFavorites();
                break;
            case ListType.WishList:
                results = GetWishList();
                break;
        }
        return results;
    }
    .
    .
    .

Cool! Simple enough, right? Just add another enum for the Christmas List, add another switch condition, and code up the Christmas List search results and call it a day. You'll look like a rock-star!

Sound good?

(This is the part in the post where I roll up a newspaper and smack you on the nose for agreeing!)

No! NO, NO, NO!

This particular way of coding violates the Open/Closed Principle. It's not at all open for extension.

What happens if our pointy-haired client gets another idea for another list? Tack on another enum and switch condition?

Umm...I don't think so.

Time to Refactor!

First, create an abstract class that has one method:

public class ListParams
{
    public string SearchTerm { getset; }
    public int PageIndex { getset; }
    public int PageSize { getset; }
    public int TotalRecords { getset; }
}
public abstract class BaseProductList
{
    public virtual IEnumerable<Product> GetResults(ListParams parameters)
    {
        return null;
    }
}

The ListParams class is to pass your parameters into the data layer to retrieve your records. You have one class for parameters. It's more manageable than seeing this:

public IEnumerable<Product> GetResults(string searchTerm, int pageIndex, int pageCount, int TotalRecords, string wishListName, string userName, string,...)

See what I mean?

Build Your Units

Make sure you have a single point of accessing your data. What I'm getting at is a Unit Of Work design pattern.

If you are building your entities through Entity Framework, then you already have a Unit Of Work design pattern. However, I like to add another layer of repositories on top of that for specific business functions.

Whatever ORM or data layer you are using, you will be calling a number of tables/stored procedures (sprocs) in your code. The Unit of Work contains all of your data access in one location.

So our interface for our ProductRepository will look like this:

IProductRepository.cs

public interface IProductRepository
{
    IEnumerable<Product> GetSearchItems(ListParams parms);
    IEnumerable<Product> GetFavoriteProducts(ListParams parms);
    IEnumerable<Product> GetWishList(ListParams parms);
    IEnumerable<Product> GetChristmasList(ListParams parms);
}

and our Unit Of Work will use that ProductRepository class using this interface.

public class UnitOfWork
{
    public IProductRepository ProductRepository { getset; }
.
.
. }

If you want more information on Unit of Work and Repository design patterns, check out the post I wrote called ASP.NET MVC Data Layer: Access Your Data Layer Through Unique Requests

Let's Get To Second Base!

Second, we need to make our individual classes. These classes will inherit from the BaseProductList.cs we created above. We could have very easily used interfaces as well.

Here are the Search Result classes.

SearchResultList.cs

public class SearchResultList : BaseProductList
{
    public override IEnumerable<Product> GetResults(ListParams parameters)
    {
        var unitOfWork = GetUnitOfWork();
        var records = unitOfWork.ProductRepository.GetSearchItems(parameters);
        TotalCount = records.Count();
        return records;
    }
.
.
.
}

FavoritesList.cs

public class FavoritesList : BaseProductList
{
    public override IEnumerable<Product> GetResults(ListParams parameters)
    {
        var unitOfWork = GetUnitOfWork();
        var records = unitOfWork.ProductRepository.GetFavoriteProducts(parameters);
        TotalCount = records.Count();
        return records;
    }
.
.
.
}

WishList.cs

public class WishList : BaseProductList
{
    public override IEnumerable<Product> GetResults(ListParams parameters)
    {
        var unitOfWork = GetUnitOfWork();
        var records = unitOfWork.ProductRepository.GetWishList(parameters);
        TotalCount = records.Count();
        return records;
    }
.
.
.
}

Now that we have our classes in place, it's VERY simple to add in a new product lists to extend the functionality of our existing classes. THIS is what was described as the Open/Closed Principle.

The switch statement had to be revisited and add a new enum and new code where with this code, we refactored non-extendible code into reusable classes, complete with class extensions.

What was our task?

SQUIRREL!

Oh yeah...we had to create a Christmas list from a list of products for our clients' site.

Now you can see how easy this is to extend this class based on our refactoring (shown below).

ChristmasList.cs

public class ChristmasList : BaseProductList
{
    public override IEnumerable<Product> GetResults(ListParams parameters)
    {
        var unitOfWork = GetUnitOfWork();
        var records = unitOfWork.ProductRepository.GetChristmasList(parameters);
        TotalCount = records.Count();
        return records;
    }
.
.
.
}

Now how do we use it?

Based on the page that you are on or the specific products that you need, you simply instantiate the class you need for your results.

For example, if you are on the Christmas List page, you can call the product results like:

var parms = new ListParams
{
    PageIndex = 1,
    PageSize = 20
};
var search = new ChristmasList();
var list = search.GetResults(parms);
return list;

Looking for your Favorite products list? Instantiate the FavoritesList on the favorites page:

var parms = new ListParams
{
    PageIndex = 1,
    PageSize = 20
};
var search = new FavoritesList();
var list = search.GetResults(parms);
return list;

One last thing...

I almost forgot about the most important benefits regarding the refactoring of this code: Testing.

Which would you want to test: the switch statement or a set of composable classes that can be mocked and provide a more granular approach to testing?

Conclusion

When someone sees this type of code and they are new to the system, it may take them a while to find out what's going on.

As I've mentioned many times before, you just need to know where to hit: To find out where the code is at and how to refactor it.

This particular technique was taken from a book called Refactoring to Patterns. I also mentioned it as one of the top books that every .NET developer should own.

I took the technique from the book and modified it to a real-world example that I built for a company. The code is running in a production environment as we speak. 

This is my first post based on my experience with refactoring code. If you are interested in learning more about real-world refactorings, please let me know and I will post more experiences.

Let me know about your recent real-world refactoring! Post your comments below!

ASP.NET 8 Best Practices on Amazon

ASP.NET 8 Best Practices by Jonathan Danylko


Reviewed as a "comprehensive guide" and a "roadmap to excellence" with over 120 Best Practices for ASP.NET Core 8, Jonathan's first book by Packt Publishing explores proven techniques for every phase of the SDLC.

Learn industry-standard concepts to improve your coding, debugging, and deployment of ASP.NET Core websites.

Order now on Amazon.com button

Picture of Jonathan "JD" Danylko

Jonathan "JD" Danylko is an author, web architect, and entrepreneur who's been programming for over 30 years. He's developed websites for small, medium, and Fortune 500 companies since 1996.

He currently works at Insight Enterprises as an Architect.

When asked what he likes to do in his spare time, he replies, "I like to write and I like to code. I also like to write about code."

comments powered by Disqus