Builder (Byggare)
Introduktion
Builder är ett skapande mönster som låter dig konstruera komplexa objekt steg för steg. Det separerar konstruktionsprocessen från representationen så att samma process kan skapa olika varianter av ett objekt.
Problem
Du vill skapa ett QueryObject för databasförfrågningar. Det kan ha tabellnamn, WHERE-villkor, JOINs, ORDER BY, LIMIT — alla valfria utom tabellnamnet.
Ett konstruktoranrop med tio parametrar är en mardröm: new Query("users", null, null, "email", true, 50, null, null, false, null). Vad betyder varje null? Ingen vet.
Lösning
Builder låter dig bygga objektet steg för steg via tydligt namngivna metoder. Du anropar bara de metoder du behöver, och anropar build() när du är klar. En Director-klass kan kapsla in vanliga kombinationer.
När ska Builder användas?
- När ett objekt har många parametrar, speciellt valfria sådana.
- När du vill kunna skapa olika representationer av samma konstruktionsprocess.
- När konstruktionslogiken är tillräckligt komplex för att förtjäna en egen klass.
Struktur
| Roll | Ansvar |
|---|---|
| Builder Interface | Deklarerar alla konstruktionssteg. |
| Concrete Builder | Implementerar stegen och håller det objekt som byggs. |
| Director | Definierar ordningen på konstruktionsstegen för vanliga konfigurationer. |
| Product | Det komplexa objekt som byggs. |
Exempel — SQL Query Builder (Java)
Scenariot: En typsäker query builder som konstruerar SQL-förfrågningar steg för steg, utan risken att blanda ihop parametrar.
// ── Product ───────────────────────────────────────────────
public class SqlQuery {
private final String table;
private final String columns;
private final String where;
private final String orderBy;
private final boolean descending;
private final Integer limit;
private final String join;
private SqlQuery(Builder builder) {
this.table = builder.table;
this.columns = builder.columns;
this.where = builder.where;
this.orderBy = builder.orderBy;
this.descending = builder.descending;
this.limit = builder.limit;
this.join = builder.join;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("SELECT ").append(columns).append(" FROM ").append(table);
if (join != null) sb.append(" ").append(join);
if (where != null) sb.append(" WHERE ").append(where);
if (orderBy != null) sb.append(" ORDER BY ").append(orderBy).append(descending ? " DESC" : "");
if (limit != null) sb.append(" LIMIT ").append(limit);
return sb.toString();
}
// ── Builder (statisk inre klass) ──────────────────────
public static class Builder {
private String table;
private String columns = "*";
private String where;
private String orderBy;
private boolean descending = false;
private Integer limit;
private String join;
public Builder from(String table) {
this.table = table;
return this;
}
public Builder select(String columns) {
this.columns = columns;
return this;
}
public Builder where(String condition) {
this.where = condition;
return this;
}
public Builder orderBy(String column) {
this.orderBy = column;
return this;
}
public Builder orderBy(String column, boolean descending) {
this.orderBy = column;
this.descending = descending;
return this;
}
public Builder limit(int count) {
this.limit = count;
return this;
}
public Builder innerJoin(String table, String on) {
this.join = "INNER JOIN " + table + " ON " + on;
return this;
}
public SqlQuery build() {
if (table == null || table.isBlank())
throw new IllegalStateException("Tabellnamn måste anges.");
return new SqlQuery(this);
}
}
}
// ── Director ─────────────────────────────────────────────
public class QueryDirector {
// Vanlig fråga: senaste aktiva användare
public SqlQuery buildLatestActiveUsers(int count) {
return new SqlQuery.Builder()
.from("users")
.select("id, name, email, last_login")
.where("is_active = 1")
.orderBy("last_login", true)
.limit(count)
.build();
}
// Komplex fråga med JOIN
public SqlQuery buildOrdersWithCustomers() {
return new SqlQuery.Builder()
.from("orders")
.select("orders.id, customers.name, orders.total")
.innerJoin("customers", "orders.customer_id = customers.id")
.where("orders.status = 'completed'")
.orderBy("orders.total", true)
.build();
}
}
// ── Klientkod ─────────────────────────────────────────────
public class Application {
public static void main(String[] args) {
QueryDirector director = new QueryDirector();
// Via Director — vanliga konfigurationer
SqlQuery latestUsers = director.buildLatestActiveUsers(10);
System.out.println(latestUsers);
SqlQuery ordersWithCustomers = director.buildOrdersWithCustomers();
System.out.println(ordersWithCustomers);
// Direkt via Builder — anpassad fråga
SqlQuery customQuery = new SqlQuery.Builder()
.from("products")
.select("name, price, stock")
.where("price < 500 AND stock > 0")
.orderBy("price")
.limit(25)
.build();
System.out.println(customQuery);
}
}
Output:
SELECT id, name, email, last_login FROM users WHERE is_active = 1 ORDER BY last_login DESC LIMIT 10
SELECT orders.id, customers.name, orders.total FROM orders INNER JOIN customers ON orders.customer_id = customers.id WHERE orders.status = 'completed' ORDER BY orders.total DESC
SELECT name, price, stock FROM products WHERE price < 500 AND stock > 0 ORDER BY price LIMIT 25
Tips: I Java är det vanligt att placera
Buildersom en statisk inre klass iProduct-klassen. Det håller koden samlad och gör det tydligt vad som byggs.
Fördelar
- Konstruera komplexa objekt steg för steg — inget behov av konstruktorer med tiotals parametrar.
- Single Responsibility Principle — Konstruktionslogiken är separerad från representationen.
- Återanvänd samma konstruktionsprocess för olika varianter via
Director.
Nackdelar
- Ökar kodens komplexitet — kräver flera nya klasser.
- Kan vara överkurs för enkla objekt med få parametrar.
Av Victor Hernandez från Bytebase.se