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 (C# / .NET)

Scenariot: En typsäker query builder som konstruerar SQL-förfrågningar steg för steg, utan risken att blanda ihop parametrar.

using System.Text;

// ── Product ───────────────────────────────────────────────

public class SqlQuery
{
    public string  Table      { get; init; } = string.Empty;
    public string  Columns    { get; init; } = "*";
    public string? Where      { get; init; }
    public string? OrderBy    { get; init; }
    public bool    Descending { get; init; }
    public int?    Limit      { get; init; }
    public string? Join       { get; init; }

    public override string ToString()
    {
        StringBuilder sb = new();
        sb.Append($"SELECT {Columns} FROM {Table}");

        if (Join is not null)
            sb.Append($" {Join}");
        if (Where is not null)
            sb.Append($" WHERE {Where}");
        if (OrderBy is not null)
            sb.Append($" ORDER BY {OrderBy}{(Descending ? " DESC" : "")}");
        if (Limit.HasValue)
            sb.Append($" LIMIT {Limit}");

        return sb.ToString();
    }
}

// ── Builder ───────────────────────────────────────────────

public class SqlQueryBuilder
{
    private string  _table      = string.Empty;
    private string  _columns    = "*";
    private string? _where;
    private string? _orderBy;
    private bool    _descending;
    private int?    _limit;
    private string? _join;

    public SqlQueryBuilder From(string table)
    {
        _table = table;
        return this;
    }

    public SqlQueryBuilder Select(string columns)
    {
        _columns = columns;
        return this;
    }

    public SqlQueryBuilder Where(string condition)
    {
        _where = condition;
        return this;
    }

    public SqlQueryBuilder OrderBy(string column, bool descending = false)
    {
        _orderBy    = column;
        _descending = descending;
        return this;
    }

    public SqlQueryBuilder Limit(int count)
    {
        _limit = count;
        return this;
    }

    public SqlQueryBuilder InnerJoin(string table, string on)
    {
        _join = $"INNER JOIN {table} ON {on}";
        return this;
    }

    public SqlQuery Build()
    {
        if (string.IsNullOrWhiteSpace(_table))
            throw new InvalidOperationException("Tabellnamn måste anges.");

        return new SqlQuery
        {
            Table      = _table,
            Columns    = _columns,
            Where      = _where,
            OrderBy    = _orderBy,
            Descending = _descending,
            Limit      = _limit,
            Join       = _join,
        };
    }

    public void Reset()
    {
        _table      = string.Empty;
        _columns    = "*";
        _where      = null;
        _orderBy    = null;
        _descending = false;
        _limit      = null;
        _join       = null;
    }
}

// ── Director ─────────────────────────────────────────────

public class QueryDirector
{
    private readonly SqlQueryBuilder _builder;

    public QueryDirector(SqlQueryBuilder builder)
    {
        _builder = builder;
    }

    // Vanlig fråga: senaste aktiva användare
    public SqlQuery BuildLatestActiveUsers(int count)
    {
        return _builder
            .From("users")
            .Select("id, name, email, last_login")
            .Where("is_active = 1")
            .OrderBy("last_login", descending: true)
            .Limit(count)
            .Build();
    }

    // Komplex fråga med JOIN
    public SqlQuery BuildOrdersWithCustomers()
    {
        return _builder
            .From("orders")
            .Select("orders.id, customers.name, orders.total")
            .InnerJoin("customers", "orders.customer_id = customers.id")
            .Where("orders.status = 'completed'")
            .OrderBy("orders.total", descending: true)
            .Build();
    }
}

// ── Klientkod ─────────────────────────────────────────────

SqlQueryBuilder builder  = new();
QueryDirector   director = new(builder);

// Via Director — vanliga konfigurationer
SqlQuery latestUsers = director.BuildLatestActiveUsers(10);
Console.WriteLine(latestUsers);

builder.Reset();
SqlQuery ordersWithCustomers = director.BuildOrdersWithCustomers();
Console.WriteLine(ordersWithCustomers);

// Direkt via Builder — anpassad fråga
builder.Reset();
SqlQuery customQuery = builder
    .From("products")
    .Select("name, price, stock")
    .Where("price < 500 AND stock > 0")
    .OrderBy("price")
    .Limit(25)
    .Build();

Console.WriteLine(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

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