3. lépés - csináljunk egy fluent buildert!

A builderről előbb beszéltem, és láttuk, hogy így lehet használni:

Icecream icecream=new IcecreamBuilder()
.addScoop("csoki")
.addSauce("csokiöntet")
.addSauce("málnaöntet")
.build();

Egyelőre még ez tök furán néz ki, de kezdük kis lépésekben! Szóval a builder egy osztály, igaz? Hozz létre egy új osztályt:

public class IcecreamBuilder {

}

Aztán, van neki egy egy csomó metódusa, amivel "felveszi a rendelést":

.addScoop("csoki")
.addScoop("málna")
.addScoop("citrom")
.setWaffel("édes tölcsér")
.addTopping("tejszínhab")
.addSauce("csokiöntet")

Kezdjük tehát!

public class IcecreamBuilder {
    // lehetnek default-ok, azaz jó kezdőértékek
    protected String waffel="sima tölcsér";

    public void setWaffel(String waffel) {
        // csak eltároljuk - "felvesszük a rendelést"
        this.waffel=waffel; 
    }
}

Oké, de a fagyik... abból többet is lehet választani! Akkor mi kell ide? Egy konténer - pont úgy, ahogy a kocsma példában gyűjöttük, hogy ki mit ivott!

public class IcecreamBuilder {
    protected String waffel="sima tölcsér";
    protected List<String> scoops=new ArrayList<>();

    public void setWaffel(String waffel) {
        this.waffel=waffel; 
    }

    public void addScoop(String scoop) {
        // adjuk hozzá a gombóc listához a választott gombócot
        scoops.add(scoop);
    }

}

A feltétek és az öntetek ugyanez a logika. Végül pedig - végül pedig tényleg össze kell építeni azt a fagyit! Hogyan? Hát az Icecream konstruktorával! Amibe nem 100 paramétert, hanem egyetlen paramétert, magát a buildert adjuk meg!

public class IcecreamBuilder {
    protected String waffel="sima tölcsér";
    protected List<String> scoops=new ArrayList<>();

    public void setWaffel(String waffel) {
        this.waffel=waffel; 
    }

    public void addScoop(String scoop) {
        // adjuk hozzá a gombóc listához a választott gombócot
        scoops.add(scoop);
    }

    public Icecream build() {
        return new Icecream(this);
    }   
}
public class Icecream {
    public Icecream(IcecreamBuilder builder) {
        // na most a builderben összerakott fagyirendelés
        // minden részlete itt van a builderben, 
        // tehát ebben a konstruktorban tökéletesen felépíthetük 
        // a kívánt fagyit 
    }       
}

Egyetlen osztályba

Elég macerás, hogy most akkor a builder miatt mindenhez két osztályt kell csinálni. Tudod mit? Nem kell! Alapvetően nem jó ötlet egy file-ba több osztályt pakolni, de a builder pattern kivétel. Itt az a szokás, hogy a fagyi és a fagyisnéni egy file-ban van - abból az okból kifolyólag, hogy innentől kizárólag a fagyisnéninek szabad fagyit csinálnia. Elvégre is, valóságban sem szoktunk a fagyispult másik oldalára osonni, és kiszolgálni magunkat, vagy Te már csináltál ilyet? :)

Tehát:

public class Icecream {
    private Icecream(IcecreamBuilder builder) {
        // na most a builderben összerakott fagyirendelés
        // minden részlete itt van a builderben, 
        // tehát ebben a konstruktorban tökéletesen felépíthetük 
        // a kívánt fagyit, de most csak kiírok pár dolgot
        System.out.println("Tölcsér: "+builder.waffel); 
        System.out.println("Ennyi gombóc: "+builder.scoops.size()); 
    }       

    public static class Builder {
        protected String waffel="sima tölcsér";
        protected List<String> scoops=new ArrayList<>();

        public void setWaffel(String waffel) {
            this.waffel=waffel; 
        }

        public void addScoop(String scoop) {
            // adjuk hozzá a gombóc listához a választott gombócot
            scoops.add(scoop);
        }

        public Icecream build() {
            return new Icecream(this);
        }   
    }
}

Inner class

A Builder most az Icecream osztályban megadott "belső" osztály, azaz inner class. Ezért úgy tudunk rá hivatkozni, hogy Icecream.Builder - ami valahol logikus, hiszen ez az "icecream buildere", ugye?

No most, akkor hogyan lesz Icecream?

// csinálunk egy példányt a builderből - a "rendelésünk"
Icecream.Builder builder=new Icecream.Builder();
// elmondjuk, mit kérünk
builder.addScoop("csoki");
builder.addScoop("narancs");
// végül megkérjük a buildert, hogy építsen ennek megfelelő fagyit
Icecream icecream=builder.build();

Hmm. Működni működik, de nem mondhatnám, hogy kézre áll...

3. lépés - csináljunk egy fluent buildert!

De mi az, hogy fluent? A fluent azt jelenti, hogy "folyékony", mármint nem úgy, mint a megolvadt fagyi, hanem inkább úgy, ahogy az ember folyékonyan beszél egy másik emberhez.

Nem-fluent:

Géza, nyisd ki az ajtót!
Géza, kapcsold fel a villanyt!
Géza, locsold meg a virágot!
Géza, kapcsold le a villanyt!
Géza, csukd be az ajtót!

Fluent:

Géza,
-nyisd ki az ajtót,
-kapcsold fel a villanyt,
-locsold meg a virágot
-kapcsold le a villanyt,
-csukd be az ajtót.

Hm, kezded érezni a különbséget, ugye?

Nem-fluent - ez tényleg nem más, mint a builder függvényeinek hívogatása:

IcecreamBuilder builder=new IcecreamBuilder();
builder.addScoop("csoki");
builder.addSauce("csokiöntet");
builder.addSauce("málnaöntet");
Icecream icecream=builder.build();

Fluent - na ez jobban néz ki, mert nem kell ismételni, hogy Géza, Géza Géza, izé builder, builder, builder:

Icecream icecream=new IcecreamBuilder()
.addScoop("csoki")
.addSauce("csokiöntet")
.addSauce("málnaöntet")
.build();

No tehát, ha valahol egymás után egy objektum metódusait hívogatjuk, és ekkor újra meg újra le kell írni a példány nevét (Géza, Géza, Géza) az nem fluent.

Ha pedig ezt sikerül megspórolni, akkor az fluent.

Hogyan lesz valami fluent?

Ó, ez lenyűgözően egyszerű! A setXXX-nek nem voidnak kell lennie, hanem vissza kell adnia a példányt saját magát (azaz a this-t).

Tehát, ahelyett, hogy void lenne a sok setXXX meg addXXX, adjanak vissza egy ... buildert, mégpedig saját magát:

public static class Builder {
    protected String waffel="sima tölcsér";
    protected List<String> scoops=new ArrayList<>();

    public Builder setWaffel(String waffel) {
        this.waffel=waffel; 
        return this; // add vissza magad
    }

    public Builder addScoop(String scoop) {
        // adjuk hozzá a gombóc listához a választott gombócot
        scoops.add(scoop);
        return this; // add vissza magad
    }

    public Icecream build() {
        return new Icecream(this);
    }   
}

Tehát, akkor most az összes setXXX meg addXXX - mivel függvény - ezért visszaad valamit, mégpedig egy Buildert, tehát a visszaadott értéknek van setXXX meg addXXX metódusa, amik visszaadnak egy Buildert, tehát lehet őket folyton-folyvást folytatni.

// csinálunk egy példányt a builderből - a "rendelésünk"
Icecream.Builder builder=new Icecream.Builder();
builder.addScoop("csoki"); //ennek az eredménye a builder maga
builder.addScoop("narancs"); //ennek az eredménye a builder maga

// végül megkérjük a buildert, hogy építsen ennek megfelelő fagyit
Icecream icecream=builder.build();
// mivel az addScoop visszaadja a buildert, írhatjuk így is:
Icecream.Builder builder=new Icecream.Builder().addScoop("csoki");

// végül megkérjük a buildert, hogy építsen ennek megfelelő fagyit
Icecream icecream=builder.build();

Az addXXX és setXXX sorokat érdemes új sorba írni, hogy ne legyenek nagyon hosszú sorok:

// mondtam már, hogy minden visszaadja a buildert?
Icecream.Builder builder=new Icecream.Builder()
.addScoop("csoki")
.addScoop("narancs")
.setWaffel("édestölcsér");

// végül megkérjük a buildert, hogy építsen ennek megfelelő fagyit
Icecream icecream=builder.build();

Ha minden visszaadja a builder-t, akkor a build metódust is lazán meg lehet hívni egyből! Azonnban, a build metódus már az igazi fagyit adja vissza:

// MINDEN visszaadja a buildert, a végén a build pedig az új fagyit.
Icecream icecream=new Icecream.Builder()
.addScoop("csoki")
.addScoop("narancs")
.setWaffel("édestölcsér")
.build();

Tulajdonképpen a builder példányra sosincs szükségünk, nem is tettem egy változóba az értékét - hiszen nekünk a fagyi a lényeg! Tehát ez történik:

new Icecream.Builder() // létrejön egy új builder, a tölcsér típusa síma tölcsér
.addScoop("csoki") // betesszük a gombócok listájába hogy csoki, visszaadjuk a buildert
.addScoop("narancs") // betesszük a gombócok listájába hogy narancs, visszaadjuk a buildert
.setWaffel("édestölcsér") // beállítjuk a tölcsér típusát, visszaadjuk a buildert
.build(); // meghívjuk az Icecream konstruktorát, és visszaadjuk az új Icecream példányt

Hmm, nem rossz, ugye? A következő részben ezt megcsináljuk a Window osztályunkkal!

Fluent metódushívás

A fluent metódushívás (fluent interface) egy példány metódusainak egymás utáni meghívása, anélkül, hogy a példány nevét újra és újra ki kellene írni. A fluent interface megadás sokkal átláthatóbbá teszi a buildert.