Builder (Építő)
Kategória: Létrehozási minta (Creational Pattern)
Lényege és célja
A Builder minta elválasztja egy összetett objektum felépítését annak megjelenítésétől, így ugyanaz az építési folyamat különböző reprezentációkat hozhat létre. Elsődleges célja a komplex objektumok lépésenkénti létrehozása.
Gyakran használjuk akkor, ha egy objektumnak sok mezője van, amelyek közül sok opcionális, vagy ha garantálni akarjuk az objektum megváltoztathatatlanságát (immutability).
Miért használjuk a konstruktor helyett?
- Olvashatóság: A
new User("Béla", "Kovács", null, 25, null, true)kód nehezen olvasható. A Builderrel ez beszédes:.firstName("Béla").age(25). - Immutabilitás: Az objektum minden mezője lehet
final. A Builder összegyűjti az adatokat, és abuild()metódus hívásakor egyszerre hozza létre a kész, módosíthatatlan példányt. - Fluent API: A metódusok visszatérnek a Builder példánnyal (
return this;), így a hívások láncolhatóak.
Miért pont statikus belső osztály (static inner class)?
Gyakori interjúkérdés, hogy a Builder miért a megépítendő osztályon belüli statikus osztály. Ennek két nyomós elméleti oka van: 1. A tyúk vagy a tojás probléma elkerülése (static): Egy sima (nem statikus) belső osztályt a Java szabályai szerint csak akkor tudnál példányosítani, ha már létezik egy példány a külső osztályból (pl. new HttpRequest().new Builder()). De a Buildert pont azért írjuk, hogy egyáltalán létre tudjuk hozni az HttpRequest-et! A static kulcsszó függetleníti a Buildert, így a külső osztály példánya nélkül is hívható. 2. A privát szféra elérése (belső osztály): Ha a Buildert egy teljesen különálló fájlba/osztályba tennénk, akkor az HttpRequest konstruktorát public-ká (vagy package-private-tá) kellene tennünk, hogy a Builder a build() metódus végén meghívhassa. Ezzel viszont a védelmünk megszűnne: bárki a világon megkerülhetné a Buildert, és közvetlenül példányosíthatna egy validálatlan HttpRequest-et. Mivel a statikus belső osztály fizikailag a külső osztály testén belül lakik, a Java megengedi neki, hogy meghívja a külső osztály private konstruktorát.
Mikor használjuk?
- Amikor egy osztálynak sok (5+) paramétere van a konstruktorban.
- Amikor el akarjuk kerülni a konstruktorok túlterhelését (overloading) különböző paraméter-kombinációkkal.
- Amikor biztonságos, szálbiztos (thread-safe) immutábilis objektumokat akarunk létrehozni.
Mermaid Diagram
A diagram szemlélteti a statikus belső osztály (Builder) és a végtermék (Product) kapcsolatát. A Builder "HAS-A" kapcsolatban áll az adatokkal, amíg össze nem állítja a végleges objektumot.
classDiagram
class HttpRequest {
-url: String
-method: String
-headers: Map~String, String~
-body: String
-HttpRequest(builder: HttpRequestBuilder)
+getUrl() String
+getMethod() String
}
class HttpRequestBuilder {
-url: String
-method: String
-headers: Map~String, String~
-body: String
+url(url: String) HttpRequestBuilder
+method(method: String) HttpRequestBuilder
+addHeader(key: String, val: String) HttpRequestBuilder
+body(body: String) HttpRequestBuilder
+build() HttpRequest
}
HttpRequest --> HttpRequestBuilder : «static inner (nested) class»
HttpRequestBuilder --> HttpRequest : build() Forráskód
package creational.builder;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
/*
* A statikus belső osztály azért a Builder pattern Szent Grálja, mert megadja mindkét világ előnyét:
* - Független: Példányosítható anélkül, hogy a külső osztály már létezne (ezért static).
* - Kiváltságos: Mivel fizikailag a külső osztály testén belül lakik,
* a Java megengedi neki, hogy meghívja a külső osztály private konstruktorát,
* ami mindenki más elől el van zárva.
*
*/
class HttpRequest {
private final String url;
private final Map<String, String> header;
private final String body;
public String getUrl() {
return url;
}
public Map<String, String> getHeader() {
return new HashMap<>(header);
}
public String getBody() {
return body;
}
private HttpRequest(HttpRequestBuilder builder) {
this.url = builder.url;
this.header = new HashMap<>(builder.header);
this.body = builder.body;
}
static class HttpRequestBuilder {
private String url;
private final Map<String, String> header = new HashMap<>();
private String body;
public HttpRequestBuilder url(String url) {
this.url = url;
return this;
}
public HttpRequestBuilder body(String body) {
this.body = body;
return this;
}
public HttpRequestBuilder addHeader(String key, String value) {
header.put(key, value);
return this;
}
public HttpRequest build() {
Objects.requireNonNull(url);
if (header.isEmpty()) {
throw new IllegalStateException("Header cannot be empty!");
}
return new HttpRequest(this);
}
}
public static HttpRequestBuilder builder() {
return new HttpRequestBuilder();
}
}
class Main {
static void main() {
var httpRequest = HttpRequest.builder()
.url("localhost.hu")
.addHeader("content-type", "json")
.addHeader("other", "xyz")
.body("Hello World")
.build();
}
}