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)); // 23Without 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 totalprint(ticket_price(2, 10, 3, True, False)) # 23Without 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)); // 23Price 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 totalterms = TicketTerms(
unit_price=10, booking_fee=3,
include_fee=True, member_discount=False)
print(ticket_price(2, terms)) # 23ticket_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
- Martin Fowler — Introduce Parameter Object.
- Microsoft — Named and optional arguments. The ticket example and comparison are original.