Function parameters in C# and the flattened sum type anti-pattern

Posted by: Yacoub Massad , on 12/24/2019, in Category Patterns & Practices
Views: 9648
Abstract: In this tutorial, I will discuss function parameters in C#. I will talk about how function parameters tend to become unclear as we maintain our programs and how to fix them.

Function Parameters – Introduction

Function parameters are part of a function’s signature and should be designed in a way to make it easier for developers to understand a function.

However, badly designed function parameters are everywhere!

In this tutorial, I will go through an example to show how a function that starts with a relatively good parameter list, ends up with a confusing one as new requirements come and the program is updated.

function parameters

The sample application

The following is an example of a function’s signature:

public void GenerateReport(int customerId, string outputFilename)

This GenerateReport function exists in some e-commerce solution that allows customers to order products. Specifically, it exists in a tool that allows people to generate reports about a particular customer. Currently, the report only contains details about customer orders.

The GenerateReport function takes two parameters as inputs: customerId and outputFilename. The customerId is used to identify the customer for which to generate the report. The outputFilename is the location on the disk to store a PDF file that contains the generated report.

I have created a GitHub repository that contains the source code. You can find it here: https://github.com/ymassad/FunctionParametersExamples

Take a look at the Tool1 project. It is a WPF based tool that looks like the following, when you run it:

wpf-report-tool

The Click event handler of the Generate Report button collects and validates the input and then invokes the GenerateReport method.

Usually, functions start with a simple parameter list. However, as the program is updated to meet new requirements, parameter lists become more complicated.

Let’s say that we have a new requirement that says that the report should optionally include details about customer complaints. That is, the report would include a list of all complaints that the customer has filed in the system. We decide to add a checkbox to the window to allow users of the tool to specify whether they want to include such details. Here is how the window in the Tool2 project looks like:

report-tool2

Here is how the signature of the GenerateReport function looks like:

public static void GenerateReport(int customerId, string outputFilename, bool includeCustomerComplaints)

The includeCustomerComplaints parameter will be used by the GenerateReport function to decide whether to include the customer complaints in the report.

Let’s say that we have another requirement which says the user should be able to specify whether to include all customer complaints or just complaints that are opened (not resolved).

Here is how the window in Tool3 looks like:

report-tool3

We added a special checkbox to satisfy the requirement. Notice how this checkbox is disabled in the figure. It becomes enabled only if the user checks the “Include customer complaints” checkbox. This makes sense because the value of the second checkbox only makes sense if the first checkbox is checked. Take a look at MainWindow.xaml in Tool3 to see how this is done.

Also notice how the new checkbox is moved a little to the right. This is to further indicate to the user that this is somehow a child of the first checkbox.

Here is how the signature of the GenerateReport method in Tool3 looks like:

void GenerateReport(int customerId, string outputFilename, bool includeCustomerComplaints, bool includeOnlyOpenCustomerComplaints)

We added a boolean parameter called includeOnlyOpenCustomerComplaints to the method which enables the caller to specify whether to include only open customer complaints or to include all customer complaints.

Although the UI design makes it clear that the value of “Include only open complaints” makes sense only if “Include customer complaints” is checked, the signature of GenerateReport method does not make that clear.

A reader who reads the signature of the GenerateReport method might be confused about which combinations of parameter values are valid, and which are not.

For example, we know that the following combination does not make sense:

GenerateReport(
    customerId: 1,
    outputFilename: "c:\\outputfile.pdf",
    includeCustomerComplaints: false,
    includeOnlyOpenCustomerComplaints: true);

Since includeCustomerComplaints is false, the value of includeOnlyOpenCustomerComplaints is simply ignored.

I will talk about a fix later.

For now, let’s look at another requirement: the ability to include only a summary of the customer complaints. That is, instead of including a list of all complaints in the report, a summary will be included that contains only:

  • The number of all complaints filed by the customer.
  • The number of open complaints.
  • The number of closed complaints.
  • The number of open complaints about shipment times.
  • The number of open complaints about product quality.
  • The number of open complaints about customer service.
  • The number of closed complaints about shipment times.
  • The number of closed complaints about product quality.
  • The number of closed complaints about customer service.

Before looking at the UI of Tool4, let’s first look at the signature of the GenerateReport method:

void GenerateReport(
    int customerId,
    string outputFilename,
    bool includeCustomerComplaints,
    bool includeOnlyOpenCustomerComplaints,
    bool includeOnlyASummaryOfCustomerComplaints)

Please tell me if you understand the parameters of this method!

Which combinations are valid, and which are not? Maybe based on the descriptions of the requirements I talked about so far, you will be able to know. But if you don’t already know about the requirements, the signature of this method provides little help.

Let’s look at the UI in Tool4 now:

wpf-report4

This is much better! We are used to radio buttons.

We know that we can select exactly one of these three options:

1. Do not include customer complaints

2. Include customer complaints

3. Include only a summary of customer complaints

We also know that if we select “Include customer complaints”, we can also choose one of the two options:

1. To include only open complaints.

2. To include all complaints, not just opened ones.

We know these options by just looking at the UI!

Now imagine that someone designed the UI of Tool4 like this:

tool-bad-ui

Would users be able to understand the options displayed in the UI? Would such UI design pass customer testing of the software?

Although you might have seen bad UI designs such as this one, I bet you have seen it fewer times compared to the code in the GenerateReport method of Tool4.

Before discussing why this is the case and suggesting a solution, let’s discuss a new requirement first: The user should be allowed to specify that they want only information relevant to open complaints when generating the summary report. That is, if such an option is selected, then only the following details are generated in the complaints summary section of the report:

  • The number of open complaints.
  • The number of open complaints about shipment times.
  • The number of open complaints about product quality.
  • The number of open complaints about customer service.

Here is how the UI looks like in Tool5:

wpf-tool-good-ui

The options in the UI looks understandable to me. I hope you think the same. Now, let’s look at the signature of the GenerateReport method:

void GenerateReport(
    int customerId,
    string outputFilename,
    bool includeCustomerComplaints,
    bool includeOnlyOpenCustomerComplaints,
    bool includeOnlyASummaryOfCustomerComplaints)

Nothing changes from Tool4. Why?

Because when writing code for the new feature, we decide that it is convenient to reuse the includeOnlyOpenCustomerComplaints parameter. That is, this parameter will hold the value true if any of the two checkboxes in the UI are checked.

We decided this because these two checkboxes are somewhat the same. They both instruct the GenerateReport method to consider only open complaints; whether when including a list of complaints, or just a summary. Also, it could be the case that this parameter is passed to another function, say GetAllComplaintsDataViaWebService that obtains complaints data from some web service. Such data will be used in the two cases; when we just want a summary of complaints, or when we want the whole list of complaints.

I am providing following excerpt from a possible implementation of the GenerateReport function to explain this point:

void GenerateReport(
    int customerId,
    string outputFilename,
    bool includeCustomerComplaints,
    bool includeOnlyOpenCustomerComplaints,
    bool includeOnlyASummaryOfCustomerComplaints)
{

    //...

    if (includeCustomerComplaints)
    {
        var allComplaintsData = GetAllComplaintsDataViaWebService(
            customerId,
            includeOnlyOpenCustomerComplaints);

        var complaintsSection =
            includeOnlyASummaryOfCustomerComplaints
                ? GenerateSummaryOfComplaintsSection(allComplaintsData)
                : GenerateAllComplaintsSection(allComplaintsData);

        sections.Add(complaintsSection);
    }

    //...
}

Such implementation pushes a developer to have a single includeOnlyOpenCustomerComplaints parameter in the GenerateReport method.

Take a look at MainWindow.xaml.cs in Tool5 to see how the input is collected from the UI and how the GenerateReport method is called.

So, the signature of GenerateReport did not change between Tool4 and Tool5. What changed are the valid (or meaningful) combinations of parameter values. For example, the following combination:

GenerateReport(
    customerId: 1,
    outputFilename: "c:\\outputfile.pdf",
    includeCustomerComplaints: true,
    includeOnlyOpenCustomerComplaints: true,
    includeOnlyASummaryOfCustomerComplaints: true);

..is not meaningful in Tool4 but is meaningful in Tool5. That is, in Tool4, the value of includeOnlyOpenCustomerComplaints is ignored when includeOnlyASummaryOfCustomerComplaints is true; while in Tool5, the value of the same parameter in the same case is not ignored.

Another confusing thing about the signature of GenerateReport in Tool4 and Tool5 is the relationship between includeCustomerComplaints and includeOnlyASummaryOfCustomerComplaints.

Is includeOnlyASummaryOfCustomerComplaints considered only if includeCustomerComplaints is true? Or should only one of these be true?

The signature does not answer these questions.

The GenerateReport method can choose to throw an exception if it gets an invalid combination, or it may choose to assume certain things in different cases.

Let’s go back to the question I asked before: why do we see bad code too much? Why is this unclarity seen more in code, than in UI?

I have the following reasons in mind:

1. UI is what the end users see from the application, and thus fixing any unclarity in this area is given a higher priority.

2. Many programming languages do not give us a convenient way to model parameters correctly. That is, it is more convenient to model parameters incorrectly.

The first point is clear, and I will not discuss it any further.

The second point needs more explanation, I think.

A function parameter list is a product type

In a previous article, Designing Data Objects in C# and F#, I talked about Algebraic Data Types: sum types and product types.

In a nutshell, a sum type is a data structure that can be any one of a fixed set of types.

For example, we can define a Shape sum type that has the following three sub-types:

1. Square (int sideLength)

2. Rectangle (int width, int height)

3. Circle (int diameter)

That is, an object of type Shape can either be Square, Rectangle, or Circle.

A product type is a type that defines a set of properties, that an instance of the type needs to provide values for. For example, the following is a product type:

public sealed class Person
{
    public Person(string name, int age)
    {
        Name = name;
        Age = age;
    }

    public string Name { get; }

    public int Age { get; }
}

This Person class defines two properties; Name and Age. An instance of the Person class should specify a value for Name and a value for Age. Any value of type string should be valid for the Name property and any value of type int should be valid for the Age property.

The constructor of the Person class should not do any validation. That is, I would say that the following is not a product type:

public sealed class Person
{
    public Person(string name, int age)
    {
        if (Age > 130 || Age < 0)
            throw new Exception(nameof(Age) + " should be between 0 and 130");

        Name = name;
        Age = age;
    }

    public string Name { get; }

    public int Age { get; }
}

It’s not a product type of string and int. Any string and int values cannot be used to construct a Person.

However, the following Person class is a product type:

public sealed class Person
{
    public Person(string name, Age age)
    {
        Name = name;
        Age = age;
    }

    public string Name { get; }

    public Age Age { get; }
}

public sealed class Age
{
    public Age(int value)
    {
        if (value > 130 || value < 0)
            throw new Exception(nameof(Age) + " should be between 0 and 130");

        Value = value;
    }

    public int Value { get; }
}

Person is a product type of string and Age. Any string and any Age can be used to construct a Person class. Age is not a product type, it is a special type that models an age of a person.

We should think about a parameter list as a product type, any combination of parameter values (i.e., arguments) should be valid. A function should not throw an exception because a certain combination of values passed to it is invalid. Instead, function parameters should be designed in a way that any combination is valid.

I am not saying that functions should not throw exceptions at all, that is a topic for a different article.

How to make all combinations valid?

The flattened sum type anti-pattern

Let’s refactor the GenerateReport method to take a parameter of type ComplaintsReportingSettings instead of the three boolean parameters. This class would simply contain the three boolean values as properties.

void GenerateReport(
    int customerId,
    string outputFilename,
    ComplaintsReportingSettings complaintsReportingSettings)
public sealed class ComplaintsReportingSettings
{
    public ComplaintsReportingSettings(
        bool includeCustomerComplaints,
        bool includeOnlyOpenCustomerComplaints,
        bool includeOnlyASummaryOfCustomerComplaints)
    {
        IncludeCustomerComplaints = includeCustomerComplaints;
        IncludeOnlyOpenCustomerComplaints = includeOnlyOpenCustomerComplaints;
        IncludeOnlyASummaryOfCustomerComplaints = includeOnlyASummaryOfCustomerComplaints;
    }

    public bool IncludeCustomerComplaints { get; }
    public bool IncludeOnlyOpenCustomerComplaints { get; }
    public bool IncludeOnlyASummaryOfCustomerComplaints { get; }
}

What is wrong with the ComplaintsReportingSettings type?

The problem is that not all combinations of the properties (or the constructor’s parameters) are valid. Let’s make this more explicit:

public sealed class ComplaintsReportingSettings
{
    public ComplaintsReportingSettings(
        bool includeCustomerComplaints,
        bool includeOnlyOpenCustomerComplaints,
        bool includeOnlyASummaryOfCustomerComplaints)
    {
        if (includeOnlyOpenCustomerComplaints && !includeCustomerComplaints)
            throw new Exception(nameof(includeOnlyOpenCustomerComplaints) + " is relevant only if " + nameof(includeCustomerComplaints) + " is true");

        if (includeOnlyASummaryOfCustomerComplaints && !includeCustomerComplaints)
            throw new Exception(nameof(includeOnlyASummaryOfCustomerComplaints) + " is relevant only if " + nameof(includeCustomerComplaints) + " is true");
        
        IncludeCustomerComplaints = includeCustomerComplaints;
        IncludeOnlyOpenCustomerComplaints = includeOnlyOpenCustomerComplaints;
        IncludeOnlyASummaryOfCustomerComplaints = includeOnlyASummaryOfCustomerComplaints;
    }

    public bool IncludeCustomerComplaints { get; }
    public bool IncludeOnlyOpenCustomerComplaints { get; }
    public bool IncludeOnlyASummaryOfCustomerComplaints { get; }
}

The added statements make sure that combinations that are not valid or that are not meaningful, will cause an exception to be thrown.

If we are designing this class for Tool4, then we would add another validation statement in the constructor:

if (includeOnlyOpenCustomerComplaints && includeOnlyASummaryOfCustomerComplaints)
    throw new Exception(nameof(includeOnlyOpenCustomerComplaints) + " should not be specified if " + nameof(includeOnlyASummaryOfCustomerComplaints) + " is true");

The ComplaintsReportingSettings class is an example of what I call the flattened sum type anti-pattern. That is, it is something that should be designed as a sum type but is instead flattened into what looks like a product type (but is actually not a product type).

Once you see a type that looks like a product type but of which there is a combination (or combinations) of its properties that is invalid or not meaningful, then you have identified an instance of this anti-pattern.

Take a look at the Tool6 project. Here is the signature of the GenerateReport method:

void GenerateReport(
    int customerId,
    string outputFilename,
    ComplaintsReportingSettings complaintsReportingSettings)

But the ComplaintsReportingSettings type looks like this:

public abstract class ComplaintsReportingSettings
{
    private ComplaintsReportingSettings()
    {
    }

    public sealed class DoNotGenerate : ComplaintsReportingSettings
    {

    }

    public sealed class Generate : ComplaintsReportingSettings
    {
        public Generate(bool includeOnlyOpenCustomerComplaints)
        {
            IncludeOnlyOpenCustomerComplaints = includeOnlyOpenCustomerComplaints;
        }

        public bool IncludeOnlyOpenCustomerComplaints { get; }
    }

    public sealed class GenerateOnlySummary : ComplaintsReportingSettings
    {
        public GenerateOnlySummary(bool includeOnlyOpenCustomerComplaintsForSummary)
        {
            IncludeOnlyOpenCustomerComplaintsForSummary = includeOnlyOpenCustomerComplaintsForSummary;
        }

        public bool IncludeOnlyOpenCustomerComplaintsForSummary { get; }
    }
} 

This is a sum type with three cases. I talked about modeling sum types in C# in the Designing Data Objects in C# and F# article. In a nutshell, a sum type is designed as an abstract class with a private constructor. Each subtype is designed as a sealed class nested inside the sum type class.

Please compare this to how the UI looks like. Here it is again (the same as Tool5):

wpf-tool-good-ui

Can you see the similarities between the sum type version of ComplaintsReportingSettings and the UI? In some sense, radio buttons together with the ability to enable/disable certain controls based on user selection of radio buttons, help us model sum types in the UI.

It is easy to understand now why developers use the flattened sum type anti-pattern: it is more convenient!

It is much easier to add a boolean parameter to a method than to create a sum type in C#.

C# might get sum types in the future. See this Github issue here: https://github.com/dotnet/csharplang/issues/113

Another reason why developers use this anti-pattern is that they are not aware of it. I hope that by writing this article, developers are more aware of it.

This anti-pattern can be more complicated. For example, it might be the case that two sum types are flattened into a single product type (in a parameter list). Consider this example:

void GenerateReport(
    int customerId,
    string outputFilename,
    bool includeCustomerComplaints,
    bool includeOnlyOpenCustomerComplaints,
    bool includeOnlyASummaryOfCustomerComplaints,
    bool includeCustomerReferrals,
    bool includeReferralsToCustomersWhoDidNoOrders,
    Maybe<DateRange> dateRangeToConsider)

This updated GenerateReport method allows users to specify whether they want to include details about customer referrals in the report. Also, it allows the users to filter out referrals that yielded no orders. The dateRangeToConsider is a bit special. This parameter is added to enable the caller to filter some data out of the report based on a date range. The parameter is of type Maybe. Maybe is used to model an optional value. For more information about Maybe, see The Maybe Monad article.

Although, it is not clear from the signature, this parameter affects not just referral data, it also effects customer complaints data when the full list of complaints is to be included. That is, it affects these data:

  • Customer referral data
  • Complaints data when a full list is requested

And it does not affect the following:

  • Complaints data when only a summary is requested.

The signature of the method in no way explains these details.

The last six parameters of the GenerateReport method should be converted to two sum types, ComplaintsReportingSettings and ReferralsReportingSettings:

public abstract class ComplaintsReportingSettings
{
    private ComplaintsReportingSettings()
    {
    }

    public sealed class DoNotGenerate : ComplaintsReportingSettings
    {

    }

    public sealed class Generate : ComplaintsReportingSettings
    {
        public Generate(bool includeOnlyOpenCustomerComplaints, DateRange dateRangeToConsider)
        {
            IncludeOnlyOpenCustomerComplaints = includeOnlyOpenCustomerComplaints;
            DateRangeToConsider = dateRangeToConsider;
        }

        public bool IncludeOnlyOpenCustomerComplaints { get; }

        public DateRange DateRangeToConsider { get; }
    }

    public sealed class GenerateOnlySummary : ComplaintsReportingSettings
    {
        public GenerateOnlySummary(bool includeOnlyOpenCustomerComplaintsForSummary)
        {
            IncludeOnlyOpenCustomerComplaintsForSummary = includeOnlyOpenCustomerComplaintsForSummary;
        }

        public bool IncludeOnlyOpenCustomerComplaintsForSummary { get; }
    }
}

public abstract class ReferralsReportingSettings
{
    private ReferralsReportingSettings()
    {
    }

    public sealed class DoNotGenerate : ReferralsReportingSettings
    {

    }

    public sealed class Generate : ReferralsReportingSettings
    {
        public Generate(bool includeReferralsToCustomersWhoDidNoOrders, Maybe<DateRange> dateRangeToConsider)
        {
            IncludeReferralsToCustomersWhoDidNoOrders = includeReferralsToCustomersWhoDidNoOrders;
            DateRangeToConsider = dateRangeToConsider;
        }

        public bool IncludeReferralsToCustomersWhoDidNoOrders { get; }

        public Maybe<DateRange> DateRangeToConsider { get; }
    }
}

Note the following:

1. Only ComplaintsReportingSettings.Generate and ReferralsReportingSettings.Generate have a DateRangeToConsider property. That is, only in these cases is the original dateRangeToConsider parameter relevant.

2. The DateRangeToConsider property in ComplaintsReportingSettings.Generate is required (does not use Maybe); while in ReferralsReportingSettings.Generate it is optional. The original parameter list in GenerateReport did not explain such details, it couldn’t have!

Here is how an updated signature would look like:

void GenerateReport(
    int customerId,
    string outputFilename,
    ComplaintsReportingSettings complaintsReportingSettings,
    ReferralsReportingSettings referralsReportingSettings)

Note that a function parameter list should always be a product type, but it should be a valid one. Here, the parameter list of the GenerateReport method is a product type of int, string, ComplaintsReportingSettings, and ReferralsReportingSettings. However, ComplaintsReportingSettings and ReferralsReportingSettings are sum types.

Conclusion:

In this tutorial, I talked about function parameters. I have demonstrated an anti-pattern which I call the flattened sum type anti-pattern, via an example. In this anti-pattern, a type that should be designed as a sum type (or more than one sum type) is designed as something that looks like a product type.

Because function parameters lists look naturally like product types and because sum types don’t have a lot of support in the C# language and the tooling around it, it is more convenient for developers to just add new parameters to the list, than to correctly design some of the parameters as sum types.

Download the entire source of this article from GitHub.

This article was technically reviewed by Damir Arh.

This article has been editorially reviewed by Suprotim Agarwal.

Absolutely Awesome Book on C# and .NET

C# and .NET have been around for a very long time, but their constant growth means there’s always more to learn.

We at DotNetCurry are very excited to announce The Absolutely Awesome Book on C# and .NET. This is a 500 pages concise technical eBook available in PDF, ePub (iPad), and Mobi (Kindle).

Organized around concepts, this Book aims to provide a concise, yet solid foundation in C# and .NET, covering C# 6.0, C# 7.0 and .NET Core, with chapters on the latest .NET Core 3.0, .NET Standard and C# 8.0 (final release) too. Use these concepts to deepen your existing knowledge of C# and .NET, to have a solid grasp of the latest in C# and .NET OR to crack your next .NET Interview.

Click here to Explore the Table of Contents or Download Sample Chapters!

What Others Are Reading!
Was this article worth reading? Share it with fellow developers too. Thanks!
Share on LinkedIn
Share on Google+

Author
Yacoub Massad is a software developer who works mainly with Microsoft technologies. Currently, he works at Zeva International where he uses C#, .NET, and other technologies to create eDiscovery solutions. He is interested in learning and writing about software design principles that aim at creating maintainable software. You can view his blog posts at criticalsoftwareblog.com


Page copy protected against web site content infringement 	by Copyscape




Feedback - Leave us some adulation, criticism and everything in between!

Categories

JOIN OUR COMMUNITY

POPULAR ARTICLES

C# .NET BOOK

C# Book for Building Concepts and Interviews

Tags

JQUERY COOKBOOK

jQuery CookBook