Refactoring techniqueOrganizing Data
Replace Array with Object
Replace an array whose positions hold different kinds of data with an object that has a named field for each.
Problem
An array (or tuple) carries unrelated values by position: row[0] is a SKU, row[1] a quantity, row[2] a warehouse code. Readers must remember the positions, and inserting a column shifts every index in the codebase.
Solution
Define a type with a named field for each position, convert the array at the point it is created, and read fields by name.
Before
// parsed from a CSV export: [sku, qty, warehouse, reorderPoint]
function lowStock(rows: [string, number, string, number][]): string[] {
return rows
.filter((r) => r[1] <= r[3])
.map((r) => `${r[0]} at ${r[2]}: ${r[1]} left (reorder at ${r[3]})`);
}
function parseRow(line: string): [string, number, string, number] {
const cols = line.split(",");
return [cols[0], Number(cols[1]), cols[2], Number(cols[3])];
}After
interface StockLevel {
sku: string;
quantity: number;
warehouse: string;
reorderPoint: number;
}
function parseRow(line: string): StockLevel {
const [sku, quantity, warehouse, reorderPoint] = line.split(",");
return {
sku,
quantity: Number(quantity),
warehouse,
reorderPoint: Number(reorderPoint),
};
}
function lowStock(levels: StockLevel[]): string[] {
return levels
.filter((s) => s.quantity <= s.reorderPoint)
.map((s) => `${s.sku} at ${s.warehouse}: ${s.quantity} left (reorder at ${s.reorderPoint})`);
}Why
- Code states meaning (
s.reorderPoint), not position (r[3]). - Adding a column changes the parser and the type, not every consumer.
- The compiler catches misspelled fields; it cannot tell
r[1]fromr[3].
Steps
- Define the type (interface, record or class) with a field per position.
- At the place the array is created, build the object instead. If many consumers remain, temporarily keep both, or add a converter.
- Change consumers one at a time from index access to field access. Run the tests after each.
- Change function signatures from the array type to the new type.
- Delete the converter and the tuple type when nothing uses them.
Watch out
- Don't apply to arrays that hold many items of the same kind. A list of SKUs is a proper array.
- Short, local tuples are idiomatic in some places, such as
[key, value]pairs fromObject.entriesor a ReactuseStatereturn. Destructure them into names instead of converting. - Wire formats and CSV column order still matter at the boundary. Keep the positional mapping in exactly one parser.
- Some hot-path numeric code uses typed arrays for memory layout. Don't convert those without measuring.
- Inverse: none worth using; positional arrays are a regression except at serialization boundaries.
Related
- Fixes Primitive Obsession and Data Clumps.
- If behavior gathers around the new type, continue with Extract Class or Replace Data Value with Object.
Agent checklist
- Positions held different meanings, not a homogeneous list.
- The positional mapping lives in one parser or adapter.
- All index accesses were replaced with named fields.
- Signatures use the new type.
- Tests pass.
Treats these smells
See also
- Replace Data Value with ObjectTurn a primitive field that has grown rules and companions into a small class that owns its data and behavior.
- Extract ClassSplit a class that does two jobs by moving one job's fields and methods into a new class it holds a reference to.
- Introduce Parameter ObjectReplace a group of parameters that always travel together with one object type, then move behavior on that group into it.
Original writing and examples. Topic coverage follows Refactoring.Guru, which explains this topic with its own illustrations.