Commissioned for this museum · after Gamma, Helm, Johnson & Vlissides, 1994
Builder
TypeScript·1994·49 lines·1060 bytes
/**
* Builder — construction spread across named steps, ending in one finished
* object.
*/
class QueryBuilder {
private readonly columns: string[] = [];
private readonly conditions: string[] = [];
private max?: number;
constructor(private readonly table: string) {}
// Each step returns `this`, which is the whole of the fluent interface.
select(...columns: string[]): this {
this.columns.push(...columns);
return this;
}
where(condition: string): this {
this.conditions.push(condition);
return this;
}
limit(rows: number): this {
this.max = rows;
return this;
}
build(): string {
const parts = [
"SELECT " + (this.columns.length ? this.columns.join(", ") : "*"),
"FROM " + this.table,
];
if (this.conditions.length) {
parts.push("WHERE " + this.conditions.join(" AND "));
}
if (this.max !== undefined) {
parts.push("LIMIT " + this.max);
}
return parts.join(" ");
}
}
console.log(
new QueryBuilder("exhibits")
.select("slug", "title")
.where("hall = 'composition'")
.limit(3)
.build(),
);Curator’s note
A builder earns its place the moment a constructor would take eight arguments, half of them optional, four of them strings. new Query("exhibits", ["slug", "title"], null, 3, false, undefined) is a line nobody can read and everybody can get wrong, and no amount of care at the call site fixes it, because the problem is that position is carrying meaning that ought to be carried by a name.
The fluent chaining is the part people copy and the least important part of the pattern. Returning this is a convenience; what matters is that construction has been broken into steps that can be named, reordered, made conditional and — the real prize — skipped. Half of the value here is in what the call site does *not* say: no columns, no limit, no ceremony explaining their absence.
The honest weakness is build. Nothing in this file requires that select was ever called, so a builder that has been half-filled is indistinguishable at compile time from one that is ready, and the failure arrives at runtime or, worse, as a silently reasonable default. Languages with richer type systems answer this with typestate, where each step returns a different type and build only exists on the type that has everything it needs. TypeScript can be persuaded to do it, at a cost in readability that usually exceeds what the safety is worth for a query with three optional parts.
Which leaves the question worth asking before reaching for this: would an options object do? Most of the time, in a language with them, it would.