I'll Be Backend
Code smells · Rust

Long parameter list: introduce a parameter object

Group the values that travel together, so a new one stops touching every signature.

13 steps4 chapters~5 min at 1.25×#long parameter list#data clump#parameter object#method signatures
The lesson plays right here. Pause anywhere, step back, or jump to a step below. Watch the Rust video on YouTube Code on GitHub

Watch the narrated video

Every step

Problem

Steps 1–4

Refactor

Steps 5–9

Payoff

Steps 10–11

Recap

Steps 12–13

The code: before and after

Before

struct ShippingService {
    rates: RateTable,
}

impl ShippingService {
    fn quote(
        &self,
        from_postal: &str, from_country: &str,
        to_postal: &str, to_country: &str,
        weight_kg: f64, length_cm: f64, width_cm: f64, height_cm: f64,
        express: bool,
    ) -> Quote {
        let price = self.price(from_country, to_country,
            weight_kg, length_cm, width_cm, height_cm, express);
        Quote::new(from_postal, to_postal, price)
    }

    fn price(
        &self,
        from_country: &str, to_country: &str,
        weight_kg: f64, length_cm: f64, width_cm: f64, height_cm: f64,
        express: bool,
    ) -> f64 {
        let volumetric_kg = length_cm * width_cm * height_cm / 5000.0;
        let rate = self.rates.per_kg(from_country, to_country);
        let cost = rate * weight_kg.max(volumetric_kg);
        if express { cost * 1.5 } else { cost }
    }
}

const WAREHOUSE_POSTAL: &str = "10115";
const WAREHOUSE_COUNTRY: &str = "DE";

struct Checkout {
    shipping: ShippingService,
}

impl Checkout {
    fn shipping_for(&self, cart: &Cart, customer: &Customer) -> Quote {
        self.shipping.quote(WAREHOUSE_POSTAL, WAREHOUSE_COUNTRY,
            &customer.postal_code, &customer.country,
            cart.weight_kg, cart.length_cm, cart.width_cm, cart.height_cm,
            cart.express)
    }
}

After

struct Address {
    postal: String,
    country: String,
}

struct Parcel {
    weight_kg: f64,
    length_cm: f64,
    width_cm: f64,
    height_cm: f64,
    fragile: bool,
}

impl Parcel {
    fn billable_kg(&self) -> f64 {
        let volumetric_kg =
            self.length_cm * self.width_cm * self.height_cm / 5000.0;
        self.weight_kg.max(volumetric_kg)
    }
}

struct ShippingService {
    rates: RateTable,
}

impl ShippingService {
    fn quote(
        &self,
        from: &Address, to: &Address, parcel: &Parcel, express: bool,
    ) -> Quote {
        let price = self.price(from, to, parcel, express);
        Quote::new(&from.postal, &to.postal, price)
    }

    fn price(
        &self,
        from: &Address, to: &Address, parcel: &Parcel, express: bool,
    ) -> f64 {
        let rate = self.rates.per_kg(&from.country, &to.country);
        let cost = rate * parcel.billable_kg();
        let cost = if parcel.fragile { cost * 1.2 } else { cost };
        if express { cost * 1.5 } else { cost }
    }
}

const WAREHOUSE_POSTAL: &str = "10115";
const WAREHOUSE_COUNTRY: &str = "DE";

struct Checkout {
    shipping: ShippingService,
}

impl Checkout {
    fn shipping_for(&self, cart: &Cart, customer: &Customer) -> Quote {
        let from = Address {
            postal: WAREHOUSE_POSTAL.to_string(),
            country: WAREHOUSE_COUNTRY.to_string(),
        };
        let to = Address {
            postal: customer.postal_code.clone(),
            country: customer.country.clone(),
        };
        let parcel = Parcel {
            weight_kg: cart.weight_kg,
            length_cm: cart.length_cm,
            width_cm: cart.width_cm,
            height_cm: cart.height_cm,
            fragile: cart.fragile,
        };
        self.shipping.quote(&from, &to, &parcel, 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 bools 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 express and fragile now sit side by side, two booleans in a row in both signatures. If someone swaps them at the checkout call, the code 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 struct. 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 borrow one Address called from, where they took two string slices. Quote reads the postal code from it, and price reads the country. Checkout builds that address from the two warehouse constants.

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 floats become a Parcel Introduce Parameter Object

The last clump is the weight and the three box sizes, so they become a Parcel struct. Four floats leave both signatures, and quote takes four parameters where it took nine. The call between them fits on one line now, and checkout builds the parcel from the cart. A Rust struct literal names every field it fills, so a width can't slip into the height by its position. 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 fields from the parcel, and it takes self without ever touching the shipping service.

Step 9: The rule moves onto Parcel Move Method

So it moves into an impl block on Parcel, where those four fields live. It reads them through self now, and price asks the parcel for its billable weight. Quote and price take four parameters each, where they took nine and seven when we started.

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 field on Parcel. Checkout copies 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 is warehouse constants, and the shipping service would then depend on checkout's types. Both addresses share one type, though, so from and to can still be swapped at the quote call, and the next lesson on domain types takes care of 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. Four string slices and four floats sat in runs of the same type, and any two in a run could trade places without the compiler noticing. On the right, two small structs hold the clumps, and the weight rule lives in the parcel. Price takes four parameters, the same four that quote takes now, 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 functions in the same order. Skip it for a function 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 floats or string slices, and functions that only pass their parameters on. Rust's standard library already works this way, because a Duration holds seconds and nanoseconds as one value, so sleep takes a single argument.