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 (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