In our webinar on “Common Antipatterns and Refactorings”, I mentioned loops could sometimes be seen as antipatterns. Some people were concerned because loops are a fundamental structure in many languages. However, don’t panic - not all loops are antipatterns! As we mention in our YouTube video, “It Depends!“:
In the webinar, we also talked about why antipatterns happen, and lack of knowledge is one of those reasons. When loops are considered an antipattern, it’s commonly because of this lack of knowledge.
In this post, I want to explain how loops can be susceptible to antipatterns and may be an antipattern as well.
Replacing a Loop with a Functional Approach
There are many antipatterns that can happen with loops. In the following loop, our goal is to calculate the total price of the remaining items that aren’t cakes. Someone who’s just starting out with C# may write the logic with this loop.
This is the loop we call out in our webinar:
public static double CalculateTotalPrice(List<BakedGood> bakedGoods)
{
double total = 0;
foreach (BakedGood bakedGood in bakedGoods)
{
if (bakedGood is Cake)
{
continue;
}
total += bakedGood.Price;
}
return total;
}
There may be other ways to write this logic. Sometimes, while you’re working in object-oriented code, you can apply functional programming techniques for calculations. In C#, LINQ allows us to take a functional approach to our collections. This approach is more readable and may be easier to maintain:
public static double CalculateTotalPrice(List<BakedGood> bakedGoods)
{
return bakedGoods
.Where(bakedGood => !(bakedGood is Cake))
.Sum(bakedGood => bakedGood.Price);
}
With the foreach
loop structure, you need to read the lines to figure out what logic is being executed. You have to see that the if
statement is an exclusion - the continue
indicates that we don’t do anything in the loop for Cake
s. We see a running total initialized before the loop, calculated as part of the loop, and then returned outside of the loop. It’s a line-by-line analysis.
With the LINQ statement, you can tell that you have your bakedGoods
collection. The Where()
is clearly a filter, and it’s easy to see the exclusion called out with the !
. The Sum()
is the calculated total based on the filtered set.
The common antipattern to watch for in this case is for
… if
or foreach
… if
. Both of these patterns in C# code may be better written with LINQ or other filtering techniques.
Infinite Loops
Another antipattern seen in loops is infinite looping. These happen often when distracted while programming. These are commonly seen with while
loops and forgetting the escape clause.
Suppose we were taking a customer’s bakery order until they say stop. Consider the following code:
bool keepAdding = true;
List<BakedGood> order = new List<BakedGood>();
while (keepAdding)
{
// ... code to add baked good to the order ...
}
Notice that we essentially have a while(true)
loop. These loops are infinite - they never end. When you find these in your code, remember to add an escape clause!
bool keepAdding = true;
List<BakedGood> order = new List<BakedGood>();
while (keepAdding)
{
// ... code to add baked good to the order ...
if (customerResponse == "STOP"){
keepAdding = false;
}
}
Complex Loop Logic
Another antipattern seen in loops is wrapping complex logic in a loop. Consider this code that is trying to find the most expensive bread in our bakery:
Bread mostExpensiveBread = null;
double maxPrice = 0;
foreach (Bread bread in breadList)
{
double discountedPrice = bread.Price;
if (bread.IsOnSale)
{
discountedPrice *= 0.9; // 10% discount
}
if (CustomerLoyaltyProgramMember)
{
discountedPrice *= 0.95; // 5% loyalty discount
}
if (discountedPrice > maxPrice)
{
mostExpensiveBread = bread;
maxPrice = discountedPrice;
}
}
One of the refactorings we can use is Extract Function. Let’s pull out the discounted price calculations into their own function:
double CalculateDiscountedPrice(Bread bread)
{
double discountedPrice = bread.Price;
if (bread.IsOnSale)
{
discountedPrice *= 0.9; // 10% discount
}
if (CustomerLoyaltyProgramMember)
{
discountedPrice *= 0.95; // 5% loyalty discount
}
return discountedPrice;
}
Once the function is extracted, that loop can then be replaced with this LINQ statement:
var mostExpensiveBread = breadList
.Select(bread => new {
Bread = bread,
DiscountedPrice = CalculateDiscountedPrice(bread)
})
.OrderByDescending(x => x.DiscountedPrice)
.FirstOrDefault()?.Bread;
Notice that by leveraging our understanding of LINQ as well as the refactoring options, our code becomes more readable and easier to maintain.
Learn More About LINQ and Functional Programming
As noted above, sometimes the loops are seen as antipatterns because there is better out there and the developer may not have that knowledge. Rest assured, we have some tips for you on learning more about LINQ and functional programming, as these can help you avoid the harder-to-read loops.
- If you aren’t familiar with LINQ, you can learn more here: 101 LINQ Samples.
- Want to learn more about functional programming? Check out this post on Everything is Functions by Kyle McMaster and Sean G. Wright. They talk about functional programming in the .NET ecosystem thanks to F#.
- Kyle McMaster talks about the
Map()
method and using it withArdalis.Result
in this blog post Transforming Results with the Map Method. - There are more helpful LINQ functions covered in this post on Exploring 7 New and Enhanced LINQ APIs in .NET 6.