A logbook for curious waters
← Software Engineering Field Guide

Long Parameter List

Before you begin: Function arguments, constructors, and simple classes.

A Long Parameter List makes a caller supply so many separate values that their order, relationships, and meaning become difficult to verify. Several values with the same type—and especially several Booleans—can make a call unreadable even before the list becomes very long.

One possible correction is to group values that form a real concept into a parameter object: an object passed as an argument. The object should explain the relationship, not merely hide the number of parameters. 1

What do these two Booleans mean?

Suppose a fictional club sells tickets using whole tokens. A purchase may include a booking fee and may qualify for a five-token member discount. The discount cannot make the price negative. Inputs in this example are nonnegative integers whose arithmetic fits in a C# integer.

Original version

public static class Tickets
{
    public static int Price(int quantity, int unitPrice, int bookingFee,
                            bool includeFee, bool memberDiscount)
    {
        int total = quantity * unitPrice;
        if (includeFee) total += bookingFee;
        if (memberDiscount) total = Math.Max(0, total - 5);
        return total;
    }
}
Console.WriteLine(Tickets.Price(2, 10, 3, true, false)); // 23

Without opening Price, a reader cannot tell whether true enables a fee, a discount, insurance, or something else. The numbers also need their positions remembered.

def ticket_price(quantity, unit_price, booking_fee,
                 include_fee, member_discount):
    total = quantity * unit_price
    if include_fee:
        total += booking_fee
    if member_discount:
        total = max(0, total - 5)
    return total
print(ticket_price(2, 10, 3, True, False))  # 23

Without opening ticket_price, a reader cannot tell whether True enables a fee, a discount, insurance, or something else. The numbers also need their positions remembered.

Group the terms that travel together

Suppose a venue applies the same unit price, fee, and discount settings to several purchases. Those values form the ticket terms; quantity varies per purchase.

Refactored version

public sealed class TicketTerms
{
    public int UnitPrice { get; }
    public int BookingFee { get; }
    public bool IncludeFee { get; }
    public bool MemberDiscount { get; }

    public TicketTerms(int unitPrice, int bookingFee,
                       bool includeFee, bool memberDiscount)
    {
        UnitPrice = unitPrice;
        BookingFee = bookingFee;
        IncludeFee = includeFee;
        MemberDiscount = memberDiscount;
    }
}

public static class Tickets
{
    public static int Price(int quantity, TicketTerms terms)
    {
        int total = quantity * terms.UnitPrice;
        if (terms.IncludeFee) total += terms.BookingFee;
        if (terms.MemberDiscount) total = Math.Max(0, total - 5);
        return total;
    }
}
TicketTerms terms = new TicketTerms(
    unitPrice: 10, bookingFee: 3,
    includeFee: true, memberDiscount: false);
Console.WriteLine(Tickets.Price(2, terms)); // 23

Price now receives the quantity and the ticket terms. Named constructor arguments also make the individual settings readable at the point where the terms are created.

from dataclasses import dataclass


@dataclass(frozen=True)
class TicketTerms:
    unit_price: int
    booking_fee: int
    include_fee: bool
    member_discount: bool


def ticket_price(quantity, terms):
    total = quantity * terms.unit_price
    if terms.include_fee:
        total += terms.booking_fee
    if terms.member_discount:
        total = max(0, total - 5)
    return total
terms = TicketTerms(
    unit_price=10, booking_fee=3,
    include_fee=True, member_discount=False)
print(ticket_price(2, terms))  # 23

ticket_price now receives the quantity and the ticket terms. Keyword arguments also make the individual settings readable at the point where the terms are created.

Notice where the complexity went

There are still four values to provide when creating ticket terms. The refactoring did not make those decisions disappear. It named and grouped them so callers can pass the same coherent set through several operations.

For a one-off call, named or keyword arguments on the original function might be the simpler improvement. If the values do not travel together elsewhere and do not form a meaningful concept, a parameter object may only move the long list into a constructor.

Also consider whether Boolean choices deserve clearer operations of their own. Grouping an ambiguous collection of flags inside a generic object named Options does not automatically explain their combinations.

The useful question is: which values belong together, and can the call express that relationship? Count the parameters only after understanding what they mean.

Sources

  1. Martin Fowler — Introduce Parameter Object.
  2. Microsoft — Named and optional arguments. The ticket example and comparison are original.