Builder — wzorzec projektowy typu kreacyjnego, który pozwala tworzyć złożone obiekty krok po kroku. W przeciwieństwie do konstruktora z dziesiątką parametrów, Builder składa obiekt poprzez łańcuch wywołań, z których każdy ustawia jedno pole. Wzorzec jest szczególnie przydatny dla obiektów z wieloma opcjonalnymi parametrami: konfiguracja klienta sieciowego, ustawienia bazy danych, budownicze alertów i nawigacji. Więcej na Refactoring Guru: Builder.
Najważniejsze
Builder (budowniczy) — kreacyjny wzorzec GoF, który oddziela konstruowanie złożonego obiektu od jego reprezentacji. Ten sam proces budowy może tworzyć różne reprezentacje. Builder jest przydatny, gdy obiekt ma wiele opcjonalnych parametrów, a konstruktor z dziesięcioma polami jest nieczytelny i nieelastyczny. Wzorzec rozwiązuje również problem Telescoping Constructor — antywzorca, gdzie liczba przeciążeń konstruktora rośnie wykładniczo.
Struktura Builder obejmuje wewnętrzną statyczną klasę Builder z polami kopiującymi pola głównej klasy. Każda metoda set zwraca Builder (this) dla fluent chaining. Finalna metoda build() tworzy docelowy obiekt, przekazując wartości pól do prywatnego konstruktora. Główna klasa ma prywatny konstruktor przyjmujący Builder. Klient: Object.builder().setField1(val1).setField2(val2).build().
Kiedy używać Builder — obiekty z 5+ polami, z których tylko 2-3 są wymagane. Obiekty konfiguracyjne (RequestConfig, DatabaseConfig). Obiekty ze złożoną logiką walidacji przy tworzeniu. Obiekty, które powinny być niezmienne (immutable) po utworzeniu. W Android Builder jest aktywnie używany w SDK: AlertDialog.Builder, Retrofit.Builder, OkHttpClient.Builder, NotificationCompat.Builder.
Kotlin Builder ma dwa podejścia: klasyczny Java-style Builder (przez klasę zagnieżdżoną) i Kotlin-style DSL builder (przez lambdę z receiverem). Java-style Builder jest preferowany dla kompatybilności z Androidem i przy użyciu z kodem Java. DSL builder — idiomatyczny sposób Kotlin: funkcja przyjmuje lambdę, wewnątrz której this jest kontekstem Builder, gdzie można bezpośrednio przypisywać pola.
// Klasyczny Builder
data class HttpConfig private constructor(
val baseUrl: String,
val timeout: Long = 30_000,
val retries: Int = 3,
val headers: Map<String, String> = emptyMap()
) {
class Builder {
private var baseUrl: String = ""
private var timeout: Long = 30_000
private var retries: Int = 3
private var headers: MutableMap<String, String> = mutableMapOf()
fun baseUrl(url: String) = apply { this.baseUrl = url }
fun timeout(ms: Long) = apply { this.timeout = ms }
fun retries(n: Int) = apply { this.retries = n }
fun header(key: String, value: String) = apply { headers[key] = value }
fun build(): HttpConfig {
require(baseUrl.isNotBlank()) { "baseUrl is required" }
return HttpConfig(baseUrl, timeout, retries, headers)
}
}
}
// Użycie
val config = HttpConfig.Builder()
.baseUrl("https://api.example.com")
.timeout(15_000)
.header("Authorization", "Bearer token")
.build()
Kotlin DSL builder — alternatywa bez klasy zagnieżdżonej. Funkcja-builder przyjmuje lambdę w kontekście obiektu-budowniczego. Jest to idiomatyczne dla Kotlin i nie wymaga pól write. DSL builders są aktywnie używane w Ktor Client, kotlinx.serialization, Compose (Modifier). DSL builder jest niekompatybilny z Java i nie nadaje się do bibliotek z Java API.
Swift Builder — Swift nie ma wbudowanego wzorca Builder, ale fluent interface można łatwo zaimplementować przez metody zwracające Self. Każda metoda ustawia właściwość i zwraca self. W przeciwieństwie do Kotlin, Swift nie wymaga osobnej klasy Builder — można zwracać sam obiekt, jeśli jest mutable na etapie składania. Dla immutable obiektów używa się zagnieżdżonej klasy Builder analogicznie do Kotlin.
struct NetworkRequest {
let url: String
let method: HTTPMethod
let headers: [String: String]
let body: Data?
let timeout: TimeInterval
final class Builder {
private var url: String = ""
private var method: HTTPMethod = .get
private var headers: [String: String] = [:]
private var body: Data? = nil
private var timeout: TimeInterval = 30
func withURL(_: String) -> Self { /* self */ }
func withMethod(_: HTTPMethod) -> Self { /* self */ }
func withHeader(key: String, value: String) -> Self { /* self */ }
func withBody(_: Data) -> Self { /* self */ }
func withTimeout(_: TimeInterval) -> Self { /* self */ }
func build() throws -> NetworkRequest {
guard !url.isEmpty else { throw BuilderError.missingURL }
return NetworkRequest(
url: url, method: method, headers: headers,
body: body, timeout: timeout
)
}
}
}
Result Builders — Swift 5.4 wprowadził @resultBuilder — mechanizm językowy do deklaratywnego budowania struktur. SwiftUI, AttributedString, SceneBuilder używają result builders. To alternatywa dla klasycznego Builder: zamiast łańcucha set-metod result builder używa bloku kodu z elementami, które kompilator zbiera w tablicę lub drzewo. @ViewBuilder w SwiftUI — najbardziej znany przykład: wewnątrz body można pisać if, switch, ForEach, a kompilator buduje View z warunków.
Telescoping Constructor — antywzorzec, przy którym klasa ma wiele przeciążonych konstruktorów z różnym zestawem parametrów. Na przykład trzy konstruktory: HttpConfig(url), HttpConfig(url, timeout), HttpConfig(url, timeout, retries). Wraz ze wzrostem parametrów liczba konstruktorów rośnie wykładniczo — dla n opcjonalnych pól potrzeba n! kombinacji. Builder rozwiązuje ten problem, pozwalając ustawiać tylko potrzebne pola.
| Cecha | Telescoping Constructor | Builder | Kotlin named args |
|---|---|---|---|
| Ilość kodu | Wykładniczy wzrost | Liniowy wzrost | Minimalna |
| Czytelność | Niska (który parametr?) | Wysoka (metoda + nazwa) | Wysoka (nazwa = wartość) |
| Niezmienność | Immutable | Immutable | Immutable |
| Kompatybilność z Java | Pełna | Pełna | Nie (tylko Kotlin) |
| Walidacja | W każdym konstruktorze | W build() — jeden raz | W init() |
Kotlin named arguments + default values — elegancka alternatywa dla Builder w czystych projektach Kotlin. Parametry konstruktora mają wartości domyślne, klient przekazuje tylko potrzebne: HttpConfig(baseUrl = url, timeout = 15_000). Wadą jest brak możliwości walidacji wymaganych pól na etapie kompilacji. Builder daje wymagane pola przez konstruktor Builder (baseUrl jest wymagany). Dla bibliotek Java Builder pozostaje standardem de facto.
Builder w Android SDK — jeden z najczęściej używanych wzorców w standardowej bibliotece. AlertDialog.Builder: new AlertDialog.Builder(context).setTitle().setMessage().setPositiveButton().create(). Retrofit.Builder: new Retrofit.Builder().baseUrl().addConverterFactory().build(). OkHttpClient.Builder: new OkHttpClient.Builder().connectTimeout().addInterceptor().build(). NotificationCompat.Builder: setContentTitle().setContentText().setSmallIcon().build().
Dlaczego Google używa Builder — wsteczna kompatybilność. Dodanie nowej metody w Builder nie łamie istniejącego kodu. Gdyby Google używał konstruktora z 20 parametrami, każde nowe pole wymagałoby nowego przeciążenia. Builder pozwala dodawać set-metody przez lata bez breaking changes. Na przykład NotificationCompat.Builder dodał setBubbleMetadata() w Android 11, nie wpływając na istniejący kod.
Builder w bibliotekach Kotlin — Ktor (HttpClientBuilder), Coil (ImageRequest.Builder), Room (Room.databaseBuilder()), Navigation (NavOptionsBuilder). W projektach Kotlin Builder często łączy się z DSL: Room.databaseBuilder(context, AppDatabase.class, "db").fallbackToDestructiveMigration().build(). Wzorzec pozostaje aktualny dla publicznych API, gdzie ważna jest wsteczna kompatybilność i interoperacyjność z Java.
Często zadawane pytania
Builder jest zbędny dla obiektów z 1-3 polami — zwykły konstruktor lub data class są czytelniejsze. Jest również zbędny w projektach Kotlin bez interoperacyjności z Java, gdzie named arguments + default values rozwiązują to samo zadanie prościej. Builder jest uzasadniony dla 5+ pól, złożonej walidacji lub Java API, gdzie named arguments są niedostępne.
Builder tworzy jeden złożony obiekt krok po kroku (ustawianie pól), Factory tworzy obiekt w całości według typu lub parametrów. Builder odpowiada na pytanie «jak złożyć?», Factory — «co utworzyć?». Builder często łączy się z Factory: Factory wybiera typ, Builder ustawia pola.
W SwiftUI rolę Builder pełnią result builders (@ViewBuilder, @SceneBuilder) i modyfikatory View (.font(), .padding()). Klasyczny Builder nie jest wymagany, ponieważ SwiftUI używa deklaratywnego podejścia i fluent modifiers. Dla komponentów UIKit Builder jest przydatny: UIAlertController, URLRequest, NSAttributedString.
Builder zwykle nie wymaga bezpieczeństwa wątkowego, ponieważ jest używany w jednym wątku do składania obiektu. Jeśli Builder jest używany w środowisku wielowątkowym (rzadki przypadek), zsynchronizuj każdą set-metodę i build(). Alternatywą jest Immutable Builder: każda set-metoda zwraca nową instancję Builder ze zmienionym polem.
Retrofit.Builder to publiczne API biblioteki, które musi działać bez kontenera DI. Builder daje elastyczność konfiguracji (baseUrl, konwertery, interceptory, niestandardowe call adapters) bez zależności od Dagger lub innych DI. Wewnątrz aplikacji DI może utworzyć Retrofit raz przez Builder, ale sam Builder pozostaje częścią publicznego API Retrofit.
Podsumowanie
Opracujemy aplikację mobilną pod klucz
IT Sectr tworzy aplikacje na iOS i Androida dla startupów i firm od 2017 roku. Doradzimy Ci i zaproponujemy najlepsze rozwiązanie.
Przeczytaj również