Skip to content

Combine Functions into Class

When several free functions all operate on the same bundle of data — each taking it as an argument, each deriving values from it — gather them into a class. The shared data becomes the object’s fields, the functions become its methods, and the data clump stops being threaded through every call.

You see a group of functions that all accept the same record and pull values out of it: baseCharge(reading), taxableCharge(reading), calculateBaseCharge(reading). The record travels from function to function as a parameter, and the functions clearly form a family around it — but nothing names that family. This is a Data Clump with behaviour orbiting it. Wrapping the data in a class gives the family a home, lets methods call each other without re-passing the data, and creates an obvious place for the next related function to land.

Three functions that each take a utility reading and compute a charge. We combine them into a Reading class whose fields are the reading’s data.

// Before — data threaded through every function
function baseRate(month: number): number {
return month >= 6 && month <= 9 ? 0.12 : 0.1;
}
function baseCharge(reading: Reading): number {
return baseRate(reading.month) * reading.quantity;
}
function taxableCharge(reading: Reading): number {
return Math.max(0, baseCharge(reading) - 30);
}
// After — data and behaviour live together
class ReadingCharge {
constructor(private reading: Reading) {}
private get baseRate(): number {
return this.reading.month >= 6 && this.reading.month <= 9 ? 0.12 : 0.1;
}
get baseCharge(): number {
return this.baseRate * this.reading.quantity;
}
get taxableCharge(): number {
return Math.max(0, this.baseCharge - 30);
}
}
flowchart LR
  subgraph Before["Before"]
    D["reading data<br/>(customer, quantity, month)"]
    F1["baseCharge(reading)"]
    F2["taxableCharge(reading)"]
    F3["calculateBaseCharge(reading)"]
    D -.-> F1
    D -.-> F2
    D -.-> F3
  end
  subgraph After["After"]
    C["Reading (class)<br/>fields: customer, quantity, month"]
    C --> M1["baseCharge()"]
    C --> M2["taxableCharge()"]
    C --> M3["calculateBaseCharge()"]
  end
  Before -.->|"Combine Functions into Class"| After
A data clump plus its orbiting functions become one class
  1. Pick the common record the functions all share and apply Encapsulate Record if it is a bare data structure — you want one object to anchor the class.
  2. Create the class (or struct + impl), taking that record in its constructor and storing it as a field.
  3. Move each function into the class one at a time. Turn its data parameter into a reference to the stored field, and run your tests after each move.
  4. Replace each former parameter access (such as reading.month) with field access on the object.
  5. As methods come to call one another, drop the now-redundant arguments they used to pass.
  6. Run your tests after every step; update each original call site to construct the object and call the method.

Use this when a set of functions clearly belongs to one piece of data and you keep passing that data between them. The class gives the cluster a name, removes the repetitive parameter threading, and offers a natural home for related logic you will add later. It also pairs well with Split Phase and Extract Function — once functions share an object, derived values become clean queries on it.

The trade-off is that you are introducing an object where there were plain functions, which is only worth it when the functions genuinely cohere around shared data. If the functions merely happen to take a similar argument but pursue unrelated goals, a class would group things that do not belong together. The alternative gathering, when the functions produce a value rather than share mutable state, is Combine Functions into Transform.

What is the trigger for Combine Functions into Class?
What happens to the shared data when the functions become a class?
In Go and Rust, how is this refactoring expressed?
When should you NOT combine functions into a class?