7. lépés - rejtsük el a build()-eket

A builderrel meg a factory methoddal ilyen picire zsugorodott az egész ablak kódja:

public class WindowTest extends Form {
    public static void main(String[] args) {
        window("Hello")
        .add(label("Hello Geza!").build())  
        .build();
    }
}

Gondolnád, hogy az irodalomnak köze van a programozáshoz? A Kis Herceg című könyvben volt egy tanulságos mondás:

A tökéletességet nem akkor érjük el, amikor már nincs mit hozzáadni, hanem amikor már nincs mit elvenni.

Hát izé, mit lehetne ebből a kódból még elvenni? A build() hívásokat. Azoknak természetesen tudjuk, hogy mi a funkciója, hisz azok fogják létrehozni a tényleges objektumokat... azonban azoknak mi a jelentése? A jelentésük értéktelen - hisz tudjuk, hogy "minden egyes builder végén build-et kell hívni" és kész. Ez jelentést nem hordoz, ez szükségszerű vacakolás.

Szabaduljunk meg tőle! Igen ám, de hogyan? Azt is tudjuk, hogy valahol muszáj a build-et meghívni, hogy végül összeépüljenek az objektumaink. No de a valahol az lehet itt, a saját ablak kódunkban, és akkor nekünk kell ezzel foglalkozni, vagy valahova máshova besöpörhetjük a szőnyeg alá, ugye?

Miért is kell .build()-et hívni a label builderén?

Azért, hogy igazi JLabel-t adhassunk át a window add() metódusának. Hogy is nézett ki a window add metódusa?

public static class Builder {
        private String text;
        private List<Component> components=new ArrayList<>();

        public Builder text(String text) {
            this.text=text;
            return this;
        }

        // egy vezérlőt ide!
        public Builder add(Component component) {
            components.add(component);
            return this;
        }

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

Err... igen, szóval ide egy component-et kellene adni. Az meg a buildereink build metódsa csinálja. De mi lenne, ha lenne olyan add, amibe egy... egy buildert lehet megadni? És majd az add metódus meghívja a build-et?

public Builder add(Label.Builder builder) {
    components.add(builder.build()); // a .build megcsinálja a Swing vezérlőt
    return this;
}

Na és ekkor az add-ba a Label builderét adhatjuk át, és akkor majd ő hívja a buildot, és nekünk nem kell:

public class WindowTest extends Form {
    public static void main(String[] args) {
        window("Hello")
        .add(label("Hello Geza!")) // nincs build, trallala!    
        .build();
    }
}

Húúha! Ejha!

Bármilyen vezérlő hozzáadása - interface

No eddig jó, de izé, most beleragasztottuk a Window kódjába, hogy az add csak a Label.builderével működik. Emiatt más vezérlőt nem lehet beletenni az ablakba, mert azoknak View.Builder, meg Edit.Builder meg mindenféle-fajta buildere lesz. Csak nem fogunk annyiféle add metódust csinálni, nem? Fura is lenne:

public Builder add(Label.Builder builder) 
public Builder add(View.Builder builder) 
public Builder add(Edit.Builder builder)

No de akkor hogyan tudunk olyat csinálni, hogy bármely builderrel működjön a dolog?

Ilyesmit már csináltunk, a kocsmában, amikor az italok öröklődtek egy közös, Innivaló ős-osztályból, így Jack és Jane bármilyen innvaló fajtát meg tudott inni.

Itt is ezt kell tenni! Lennie kell egy ős Builder osztálynak - nevezzük ComponentBuildernek-, amiből származnak a mi buildereink. A közös dolog a mi buildereinkben, hogy mindegyiknek kell lennie egy Component build() metódusnak, ami legyártja a Swing vezérlőt, amit majd kirakunk a képernyőre. Szóval, az ős-osztályban egyetlen ilyen metódusnak kell lennie, és mivel a többi Builder ebből származik, ezért ezek override-olják ezt a metódust. Végül, a Window kódjában nem Label.Builder-t fogunk paraméterként elfogadni, hanem ezt az ős-buildert.

(Emlékszel: Jack nem Sört meg Bort iszik, hanem Innivalót - és mivel a Sör meg a Bor is Innivaló leszármazott, ezért Jack bármit meg tud inni, ami Innivaló leszármazott. Itt is ez a logika: a Window-ba nem Label.Builder-t meg View.Builder-t dobálunk, hanem ComponentBuildert. Mivel a Label.Builder és a View.Builder is ComponmentBuilder leszármazott, ezért ezeket simán tudja kezelni a Window add metódusa.)

Sima osztály, vagy Abstract class?

No tehát, a közös ős legyen ComponentBuilder, aminek van egy Build metódusa:

public class ComponentBuilder {
    public Component build() {
        return null; // izé, mi semmit nem tudunk még építeni
    }
}

A Label.Builder pedig ebből származzék, és override-oljuk a build metódust, hogy éppen labelt építsen:

public static class Builder extends ComponentBuilder {
    private String text;

    public Builder text(String text) {
        this.text=text;
        return this;
    }

    @Override
    public Component build() {
        return new Label(this);
    }
}

Hurrá! No ez működik is, de csinálhatnánk szebben! Mi ebben a gond? A gond az, hogy ha esetleg elfelejtük override-olni a build metódust, akkor a megöröklött build metódust fogja meghívni az add, ami pedig izé... null-t ad vissza. Mert ugye, mit adjon vissza az ős osztály, Üvegtigrist vagy Fapumát? Igazából az ős osztály semmi értelmeset nem tud buildelni, nem?

Valahogyan mondjuk meg a Javanak, hogy márpedig minden leszármazottnak muszáj override-olni a build metódust! Erre szolgál az abstract class (elvonatkoztatott osztály?) Úgy kell érteni, hogy egy olyan osztály, ami önmagában még semmit nem tud csinálni, legalább egy metódusát a leszármazottaknak kell ténylegesen megírni.

Figyeld az abstract szavakat! A metóduson az abstract azt jelenti, hogy nincs megírva a belseje. Nincs kapcsos zárójel, nincs semmi, csak a neve és a paraméterei. Az osztályon az abstract azt jelenti, hogy van legalább egy abstract metódusa.

public abstract class ComponentBuilder {
    public abstract Component build();
}

Ha most esetleg elfelejtjük a leszármazott Label.Builderben megadni, hogy mit csináljon a metódus, már a szerkesztőben hibát kapunk:

Hoppá, nem csináltunk saját build metódust!

Sőt, ha odakattintasz az Add unimplemented methods automatikus javításra, akkor bele is teszi a hiányzó metódust - és már csak a belsejét kell megírni. Nem rossz, ugye? :)

Abstract class, vagy Interface?

Valójában a mi ős-osztályunk teljesen üres. Nincs benne semmilyen attribútum (változó) és egyetlen metódust sem tartalmaz (csak az abstract metódust, amit majd a leszármazottnak kell implementálni, azaz megírni a belsejét).

Pont erre van egy picit jobb megoldás, az Interface. Az Interface nem más, mint egy ígéret arra, hogy bizonyos metódusokat majd a leszármazott implementálni fog. Eléggé hasonlít arra, amit az abstract classal csináltunk.

Két különbség: nem abstract class, hanem interface a típusa az izének - osztálynak? Ez nem osztály, hisz nincs benne attribútum (változó) és semmiféle metódust sem tartalmazhat (csak metódusok neveit és paramétereit) - szóval ez nem osztály, hanem interface. (Ennek semmi köze nincs a fluent interface dologhoz, csak véletlenül hasonlít a nevük.)

public interface ComponentBuilder {
    Component build();
}

A leszármazott osztály pedig... izé, most mondtuk, hogy az interface nem osztály, tehát akkor származni se lehet belőle! Ezt úgy mondjuk, hogy "implementálja" azaz megvalósítja az interfészben ígért metódusokat. Szóval nem extends, hanem implements.

public static class Builder implements ComponentBuilder {
    private String text;

    public Builder text(String text) {
        this.text=text;
        return this;
    }

    @Override
    public Component build() {
        return new Label(this);
    }
}

Minden más ugyanaz. De akkor most mi az előnye az interface-nek az abstract classhoz képest? Jó kérdes! Az előnye hatalmas: egy osztály egyetlen másik osztályból származhat, viszont tetszőlegesen sok interfészt implementálhat.

Példaképp nézzünk meg egy ilyet:

public class Circle extends Shape implements Area,Circumfence {
    private double r;

    public Circle(double x,double y,double r) {
        // a Shape-től örököltünk getX, setX, getY, setY dolgokat
        // meg a Shape(double x,double y) konstruktort
        super(x,y);
        // a sugár már a kör sajátossága
        this.r=r;
    }

    // Az Area interface-ben megígértük, hogy kiszámoljuk a területünket
    @Override 
    public double getArea() {
        return  r*r*3.14;
    }

    // A Circumfence interface-ben megígértük, hogy kiszámoljuk a kerületet
    @Override 
    public double getCircumfence() {
        return  2*r*3.14;
    }
}

No és ekkor egy Circle példány egyszerre:

Azaz át lehet adni olyan helyre paraméternek, ami Circle, vagy Shape, vagy Area, vagy Circumfence típusú paramétert vár - hiszen a mi Circle osztályunk Shape leszármazott (a Bor Innivaló), és az Area és a Circumfence interfészekben beígért metódusokat is megcsináltuk.

Ilyet sima örökléssel nem tudunk csinálni, mert a Java nem támogatja a többszörös öröklést (teljesen jó okokból - amiknek a magyarázata kicsit messzire vezet, ezért kihagyom.)

Class, abstract class, vagy interface?

Ha egy ős-osztály minden metódust értelmesen tud implementálni (tehát nem nullokat kell visszaadnia, csak azért mert valamit muszáj) akkor sima class legyen az ős. Ha egy ős-osztályban vannak megörökölendő attribútumok, vagy a nem-teljes metódusokon kívül teljesen értelmes metódusok is, akkor abstract class. Ha az ős osztályban kizárólag olyan metódusok vannak, amiket kizárólag a leszármazott tud implementálni, akkor interface.

Jane megtanul autót vezetni

Vágod, hogy mi az az interface (itt a programozásban)? Ha még picit bizonytalan vagy, akkor nézzük meg a dolgot más irányból. Az interface egy ígéret-lista, hogy azokat a metódusokat egy leszármazott osztály (mint például a mi Label.Builder osztályunk) tartalmazni fogja.

Más metódusoknak innentől kezdve lehet interface típusú paramétere - hiszen számíthatnak rá, hogy oda csak olyan osztályt lehet átadni, ami betartja az ígéretet, és implementálja az interface metódusait.

A valódi életben is sokszor használjuk ugyanezt. Például, Jane megtanul autót vezetni. Tudja, hogy a kormány csavarásával az irányt, a gázpedállal pedig a sebességet tudja változtatni. Ez a Vezethető interface - azaz kormány, gázpedál kezelése.

Az összes Autó implementálja a Vezethető interfészt - mindegyikben van kormány, és gázpedál, ezért Jane nem csak egyetlenegy autót tud vezetni, hanem az összes gyártó összes féle autóját, amiben van kormány és gázpedál.

A Tankokkal Jane nem boldolgul - mert ott botkormány van, tehát a Tank nem implementálja azt a Vezethető interfészt, amit Jane tudna használni.

Azonban, meglepő módon a Trolibusz, Autóbusz is megy. Sőt, Jane rájött, hogy a JetSky-t és a legöbb Hajót is tudja vezetni - mert célszerű módon azok is betartják a Vezethetőség ígéretét: kormány, gázpedál.

Jane nagyon örül, hogy nem kell külön-külön megtanulnia Volksvagent, Toyotát, BMW-t, Trolibuszt, Ikarus autóbuszt, Scania autóbuszt, stb. vezetni - hiszen ezekre okosan egyforma kormányt és gázpedált tettek a tervezők. Nagyon csodálatos :)

Akkor tegyük össze a ComponentBuildert!

Tehát egy ős-interface:

public interface ComponentBuilder {
    Component build();
}

és a Label.Buildere:

public static class Builder implements ComponentBuilder {
    private String text;

    public Builder text(String text) {
        this.text=text;
        return this;
    }

    @Override
    public Component build() {
        return new Label(this);
    }
}

Természetesen a többi builder is ilyen lesz, a View.Buildere, az Edit.Buildere, stb. Mind megígérik, hogy lesz neki build metódusa.

A Window bilderében pedig egy olyan paramétert várunk az add metódusban, aki megígéri, hogy lesz neki build metódusa, szóval egy ComponentBuilder:

public static class Builder {
        private String text;
        private List<Component> components=new ArrayList<>();

        public Builder text(String text) {
            this.text=text;
            return this;
        }

        // bármely ComponentBuilder jöhet
        public Builder add(ComponentBuilder builder) {
            components.add(builder.build);
            return this;
        }

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

Innentől kezdve szabad a gazda:

public class WindowTest extends Form {
    public static void main(String[] args) {
        window("Hello")
        // ez épp Label.Builder lesz, ami ComponentBuilder
        .add(label("Hello Geza!"))  
        // ez épp Akarmi.Builder lesz, ami ComponentBuilder
        .add(akarmi("Hello Geza!"))     
        .build();
    }
}

Ős-izék

Ha közös őst akarunk, akkor az lehet class (sima osztály), abstract class (absztrakt osztály), vagy interface (interfész - azaz ígéret metódusokra). A három módszer egyformán jelent közös őst, ugyanakkor különbözik abban, hogy mikor használjuk.