dateo. Coding Blog

Coding, Tech and Developers Blog

csharp
tuples
clean code

Why tuples in C# are not always a code smell

Dennis Frühauff on January 4th, 2023

Today's article will focus on tuples and show examples of tuples in C# being a code smell. But also, I want to point out when tuples can actually help you make your code more readable and cleaner. The trick is not to mistake tuples for the hammer for every screw out there. Let me show you how.


There comes a time in many software developer careers in .NET (and various other languages as well) when people discover tuples as the general tool to write their code. I tend to find that true, especially in people's very early career stages (and I am certainly guilty of that myself) when Clean Code is still something that needs to be cut from the only roughly-shaped developer.


But first, let's quickly have a look at what a tuple is. We can think of it as a simple semantic grouping of values inside our code:


(double, int) myTuple = (4.9, 15);
// or simpler
var myTuple = (4.9, 15);

Accessing the "properties" of this tuple can be done using automatically generated accessors:


var temp = myTuple.Item1;

These accessors can be renamed in favor of more speaking terminology, like so:


(double MyDouble, int MyInt) myTuple = (4.9, 15);
var temp = myTuple.MyDouble;

That's it. Well, not quite, but you might want to look the details up in Microsoft's own documentation.
Looks like a small class, doesn't it?


Where do we often see tuples being used?

There is a rather frequent refactoring that is being made to methods that have an out parameter in their signature. Consider an example in which some code tries to validate a Customer class. You might come across a method like this:


bool IsValid(Customer customerToValidate, out string reason);

How did this come into being? At first, probably, returning true or false for the validation seemed to be enough until one day, the business needed additional information to be displayed somewhere. As an easy fix, the developer tasked with this job added the out parameter to this method. Not beautiful, but did the job, I guess.


As the business rules get more complicated, consider we'd have to make this method asynchronous, maybe an async call to a repository needs to be made within. Whatever the reason, out parameters are not possible in asynchronous methods. Consequently, someone might have done the obvious: Introduce a tuple.


Task<(bool IsValid, string Reason)> IsValidAsync(Customer customerToValidate);

The fact that we have the ability to name the values inside of tuples (which came with C# 7), actually does not make this code bad. Personally, I still consider using tuples on these occasions to be code smell and there are a couple of reasons for it:


  1. The return value of the method is not self-descriptive. You need to see the method name or the context to know what's going on.
  2. If more data needs to be returned, the method signature needs to be changed and, consequently, all references need to be changed as well.
  3. Also, with more data returned in this case, things can quickly get out of hand. This is like having too many parameters for a single method: You'd probably need a new class.

My favorite way of doing a refactoring to this method is to use a small record:


public async Task<ValidationResult> IsValidAsync(Customer customerToValidate)
{
  ...
}

public record ValidationResult(bool IsValid, string Reason);

This small change helps future me easily identify what the data are about. And if more properties are necessary, the method signature and calls do not change.


All that being said, I am not a fan of tuples and try to avoid them generally. There are however situations, where tuples can help you write very readable code.


Use tuples to your advantage

Say that we wanted to calculate a specific discount for our customers based on the following rules:


  • Premium customers get 20% off.
  • Gold customers get 30% off.
  • Regular customers, when they are students (< 25 years), get 10% off.
  • Regular adult customers get no discount.
  • All regular customers get 15% off during happy hour (3 to 8 p.m.).

If we implement these rules right away, we could end up with something like this:


public decimal CalculateDiscount(Customer customer, DateTime time)
{
    if (customer.CustomerType == CustomerType.Gold)
    {
        return 0.3m;
    }
    else if (customer.CustomerType == CustomerType.Premium)
    {
        return 0.20m;
    }
    else
    {
        if (time.Hour is > 15 and < 20)
        {
            return 0.15m;
        }

        if (customer.Age < 25)
        {
            return 0.1m;
        }
        else
        {
            return 0m;
        }
    }

    return 0m;
}

That is almost 30 lines of nested boilerplate code. Of course, we could easily bring that down to only a couple of lines by reducing the number of brackets and unnecessary else statements. But still, if we stick to this kind of information flow, we are still left with code that is not easily readable, especially considering that the business rules are pretty simple. They are likely to change or grow, and then this piece of code will get ugly in no time.


But here is the thing: If we make use of the fact, that tuples can be pattern matched, we might end up with this:


public decimal CalculateDiscount(Customer customer, DateTime time)
{
    return (IsStudent(customer), IsHappyHour(time), customer.CustomerType) switch
    {
        (_,         _, CustomerType.Gold)    => 0.3m,
        (_,         _, CustomerType.Premium) => 0.2m,
        (_,      true, CustomerType.Regular) => 0.15m,
        (true,  false, CustomerType.Regular) => 0.10m,
        (false, false, CustomerType.Regular) => 0.0m
    };
}

public bool IsStudent(Customer customer) => customer.Age < 25;
public bool IsHappyHour(DateTime datetime) => datetime.Hour is > 15 and < 20;

Doesn't this pretty much look like the business rules stated above? In this case, creating a temporary tuple holding the necessary information and feeding it into C#'s pattern-matching engine reduced the complexity of our code significantly. The business rules can be directly read from our code and are easily extendable. Please also note that I made use of the discard (_) variable for cases where only part of the tuple is actually relevant for the business decision.


TLDR;

There is quite a debate out there on whether tuples in C# should be considered a code smell. Some of the public discussions are old and some are newer. Without engaging in any of these discussions directly, I personally tend to avoid tuples in general. But I also admit that there are certain situations in which they are helpful. So, let me share my general recommendations:


  • Tuples are an easy way of fast-prototyping functionality. Since they do get out of hand rather quickly, I'd say there is always time to replace them with a simple POCO.
  • If you do want to use tuples, use them internally, i.e., inside the logic of a class or method. You should certainly avoid exposing tuples via public access modifiers or even in interfaces.
  • In pattern-matching scenarios, they can be put to good use, making for concise code that handles business decisions.

Let's hear from you:



Please share on social media, stay in touch via the contact form, and subscribe to our post newsletter!

Be the first to know when a new post was released

We don’t spam!
Read our Privacy Policy for more info.

We use cookies on our website to give you the most relevant experience by remembering your preferences and repeat visits. By clicking “Accept All”, you consent to the use of ALL the cookies. However, you may visit "Cookie Settings" to provide a controlled consent.