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

RollAnsvar
Builder InterfaceDeklarerar alla konstruktionssteg.
Concrete BuilderImplementerar stegen och håller det objekt som byggs.
DirectorDefinierar ordningen på konstruktionsstegen för vanliga konfigurationer.
ProductDet 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 Builder som en statisk inre klass i Product-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