Kihagyás

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?

  1. 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).
  2. Immutabilitás: Az objektum minden mezője lehet final. A Builder összegyűjti az adatokat, és a build() metódus hívásakor egyszerre hozza létre a kész, módosíthatatlan példányt.
  3. 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();

    }
}