Long parameter list: introduce a parameter object
Group the values that travel together, so a new one stops touching every signature.
Watch the narrated video
Every step
Problem
Steps 1–4Refactor
Steps 5–9Payoff
Steps 10–11Recap
Steps 12–13The code: before and after
Before
struct ShippingService {
let rates: RateTable
func quote(fromPostal: String, fromCountry: String,
toPostal: String, toCountry: String, weightKg: Double,
lengthCm: Double, widthCm: Double, heightCm: Double,
express: Bool) -> Quote {
let cost = price(fromCountry: fromCountry, toCountry: toCountry,
weightKg: weightKg, lengthCm: lengthCm,
widthCm: widthCm, heightCm: heightCm,
express: express)
return Quote(fromPostal: fromPostal, toPostal: toPostal, price: cost)
}
private func price(fromCountry: String, toCountry: String,
weightKg: Double, lengthCm: Double,
widthCm: Double, heightCm: Double,
express: Bool) -> Double {
let volumetricKg = lengthCm * widthCm * heightCm / 5000
var cost = rates.perKg(from: fromCountry, to: toCountry) *
max(weightKg, volumetricKg)
if express { cost *= 1.5 }
return cost
}
}
enum Warehouse {
static let postal = "10115"
static let country = "DE"
}
struct Checkout {
let shipping: ShippingService
func shippingQuote(for cart: Cart, to customer: Customer) -> Quote {
return shipping.quote(
fromPostal: Warehouse.postal, fromCountry: Warehouse.country,
toPostal: customer.postalCode, toCountry: customer.country,
weightKg: cart.weightKg, lengthCm: cart.lengthCm,
widthCm: cart.widthCm, heightCm: cart.heightCm,
express: cart.express)
}
}After
struct Address {
let postal: String
let country: String
}
struct Parcel {
let weightKg: Double
let lengthCm: Double
let widthCm: Double
let heightCm: Double
let fragile: Bool
var billableKg: Double {
let volumetricKg = lengthCm * widthCm * heightCm / 5000
return max(weightKg, volumetricKg)
}
}
struct ShippingService {
let rates: RateTable
func quote(from: Address, to: Address, parcel: Parcel,
express: Bool) -> Quote {
let cost = price(from: from, to: to, parcel: parcel,
express: express)
return Quote(fromPostal: from.postal, toPostal: to.postal,
price: cost)
}
private func price(from: Address, to: Address, parcel: Parcel,
express: Bool) -> Double {
var cost = rates.perKg(from: from.country, to: to.country) *
parcel.billableKg
if parcel.fragile { cost *= 1.2 }
if express { cost *= 1.5 }
return cost
}
}
enum Warehouse {
static let postal = "10115"
static let country = "DE"
}
struct Checkout {
let shipping: ShippingService
func shippingQuote(for cart: Cart, to customer: Customer) -> Quote {
let from = Address(postal: Warehouse.postal,
country: Warehouse.country)
let to = Address(postal: customer.postalCode,
country: customer.country)
let parcel = Parcel(weightKg: cart.weightKg,
lengthCm: cart.lengthCm,
widthCm: cart.widthCm,
heightCm: cart.heightCm,
fragile: cart.fragile)
return shipping.quote(from: from, to: to, parcel: parcel,
express: cart.express)
}
}Transcript, step by step
Problem
Step 1: A quote with nine parameters Before
At checkout, this method asks the shipping service for a quote. It passes the warehouse address from two constants, then the customer's address, then five values from the cart. The quote method takes all nine, and hands seven of them on to the price method. Price works out a volumetric weight from the box size, charges for whichever weight is larger, and adds half again for express.
Step 2: One flag, five edits Code smell
The carrier now charges twenty percent more for fragile parcels. Price needs a fragile flag, so the quote method, the call inside it and the checkout call all have to carry one there. A long parameter list is a method that takes so many values that every caller must line them up in the right order. You can see it here in the quote signature, here in the price signature, here in the call inside quote, and here in the checkout call. Quote uses two of its ten values itself, and the other eight pass straight through to price.
Step 3: Two booleans side by side Code smell
One new value cost four edits to signatures and calls before we reached the one line that uses it. And now express and fragile sit side by side as two booleans in every list. Swift's labels name each slot, but both values are booleans, so passing the cart's fragile flag as express still compiles, and the customer pays the wrong surcharge.
Step 4: Back it out, then group Plan
Let's back that change out and refactor first. Look at which values travel together, because the same groups show up in every list. The origin postal code and country always come as a pair, so do the destination's, and so do the weight and the three box sizes. A group like that is called a data clump, and Introduce Parameter Object turns each clump into one small type. We'll do it one clump at a time, and after each step the code compiles and the price comes out the same.
Refactor
Step 5: The origin becomes an Address Introduce Parameter Object
The origin comes first, so we add an Address struct with a postal code and a country. Quote and price now take one Address labeled from, where they took two strings. Quote reads the postal code from it, and price reads the country. Checkout builds that address from the two warehouse constants, with the initializer Swift writes for every struct.
Step 6: The destination is an Address too Introduce Parameter Object
The destination is the same kind of clump, so it reuses the same Address type. Both methods take from and to now, and the quote reads both postal codes off them. Checkout builds the second address from the customer.
Step 7: Four doubles become a Parcel Introduce Parameter Object
The last clump is the weight and the three box sizes, so they become a Parcel struct. Four doubles leave both signatures, and quote takes four parameters where it took nine. Checkout builds the parcel from the cart, next to the two addresses. Price now reaches into the parcel four times to work out the weight it charges for.
Step 8: Name the weight rule Extract Method
That weight rule deserves a name, so we extract it into a method called billable kg. Price multiplies the rate by billable kg, and no longer cares how that weight is worked out. But the new method reads four values from the parcel and nothing from the shipping service.
Step 9: The rule moves onto Parcel Move Method
So it moves onto Parcel, where those four values live, and in Swift a value like this reads best as a computed property. Inside the struct it reads the properties directly, and price asks the parcel for its billable weight. Quote and price take four parameters each now, and the call between them passes the two addresses, the parcel and one flag.
Payoff
Step 10: Fragile, in three edits Payoff
Now the fragile surcharge goes back in. Fragile is a fact about the parcel, so it becomes one more property on Parcel, and the memberwise initializer picks it up. Checkout passes the cart's flag where it builds the parcel, and price adds the surcharge, so that's three edits, each where the value is made or used. The quote signature, the price signature and the call inside quote didn't change.
Step 11: Why the clumps paid off Payoff
Each data clump is one type now, so a new fact about the parcel joins Parcel, and quote, which only passes values on, stays the same. And the two booleans no longer sit side by side, because express is a parameter and fragile lives inside the parcel. Passing the whole cart and customer would look shorter, but the origin comes from the warehouse constants, and the shipping service would then depend on checkout's types. The labels name every value, but from and to are both addresses and the four sizes are all doubles, so a swapped pair still compiles, and the next lesson's domain types fix that.
Recap
Step 12: Where we started, where we ended Before / after
On the left, quote took nine parameters and handed seven of them on to price. Two country strings, four doubles and a boolean sat in a row, and the call spelled out every one of them twice. On the right, two small structs hold the clumps, and the weight rule lives in the parcel. Quote and price take four parameters each, and fragile is already in.
Step 13: When to reach for it Takeaways
Reach for a parameter object when the same group of values goes through several methods in the same order. Skip it for a method with a few unrelated values, like a log call with a level, a message and an error, because a wrapper would group things that don't belong together. You'll spot it by the same names repeated across signatures, long runs of doubles or strings, and methods that only pass their parameters on. Foundation already works this way, because Calendar takes one Date Components value that holds the year, the month and the day.