Data Clumps
in reviewThis lesson is complete and being validated before release - details may still change.
in reviewThis lesson is complete and being validated before release - details may still change.
data-clumps.ts
import type { Program } from "./program";import type { Tracer } from "../trace";import type { SmellDetector } from "./detector";/** A group of parameter names that travel together through more than one signature. */export interface Clump { readonly id: number; /** The names, in the order the first owner declares them. */ readonly names: readonly string[]; /** The declared type of each name, from the first owner. */ readonly types: readonly string[]; /** Every method whose signature holds all the names: "Hotel.reserve". */ readonly owners: readonly string[]; /** How many owners: the number of signatures the clump is spread over. */ readonly count: number;}export interface ClumpReport { /** Signatures examined: every method of every class. */ readonly signatures: number; readonly clumps: readonly Clump[]; /** The most names any clump holds; 0 when there is none. */ readonly largest: number; readonly smelly: boolean;}/** One method's parameter list, as the detector sees it. */interface Signature { readonly owner: string; readonly className: string; readonly method: string; readonly names: readonly string[]; readonly types: ReadonlyMap<string, string>;}/** * Fowler's Data Clumps, in parameter form: the same few names that * always appear together in signatures (`start, nights, guests`) and * want to be one object. Every method of every class contributes its * signature; every pair of signatures with a shared name is an * `overlap`; a shared set of at least `minSize` names, seen for the * first time, is a `clump` owned by every signature that holds all of * its names. A subset of a clump that some other pair shares would be a * clump of its own: there is no suppression, so fixtures keep their * clumps disjoint. Field clumps are out of scope here; the cure for a * parameter clump is Introduce Parameter Object. * * Time: O(s² · p) over s signatures of at most p parameters. */export class DataClumps implements SmellDetector<Program, ClumpReport> { constructor( readonly minSize = 3, private readonly tracer?: Tracer, ) {} analyze(program: Program): ClumpReport { const signatures = this.signatures(program); signatures.forEach((signature, index) => { // ▸ one method's parameter names, read from its class this.tracer?.emit("signature", { className: signature.className, method: signature.method, params: signature.names.map((n) => `${n}: ${signature.types.get(n)}`), index, count: signatures.length }); }); const clumps: Clump[] = []; const seen = new Set<string>(); for (let i = 0; i < signatures.length; i++) { for (let j = i + 1; j < signatures.length; j++) { const a = signatures[i]!; const b = signatures[j]!; const shared = a.names.filter((n) => b.names.includes(n)); if (shared.length === 0) continue; // ▸ two signatures share at least one name this.tracer?.emit("overlap", { a: a.owner, b: b.owner, shared, size: shared.length }); const key = [...shared].sort().join(","); if (shared.length < this.minSize || seen.has(key)) continue; seen.add(key); const owners = signatures.filter((s) => shared.every((n) => s.names.includes(n))).map((s) => s.owner); const clump: Clump = { id: clumps.length, names: shared, types: shared.map((n) => a.types.get(n)!), owners, count: owners.length }; clumps.push(clump); // ▸ a shared set big enough and new: a clump, owned by every signature holding all of it this.tracer?.emit("clump", { id: clump.id, names: clump.names, types: clump.types, owners: clump.owners, count: clump.count }); } } const largest = clumps.reduce((max, c) => Math.max(max, c.names.length), 0); const smelly = clumps.length > 0; // ▸ the clumps found, against the smallest size that counts this.tracer?.emit("verdict", { signatures: signatures.length, clumps: clumps.length, largest, minSize: this.minSize, smelly }); return { signatures: signatures.length, clumps, largest, smelly }; } /** Every method of every class in declaration order, with its parameter names and their types. */ private signatures(program: Program): readonly Signature[] { const out: Signature[] = []; for (const module of program.modules) { for (const cls of module.classes) { for (const method of cls.methods) { out.push({ owner: `${cls.name}.${method.name}`, className: cls.name, method: method.name, names: method.params.map((p) => p.name), types: new Map(method.params.map((p) => [p.name, p.type] as const)) }); } } } return out; }}1 clump across 4 classs; the widest holds 3 names. Still data clumps.