tifyty

pure Java, what else ?

Egy kis fejtörő, autós játék

Modellezzünk autókat, és eresszük el egy kicsit az Objektum orientáltságot.

Legyen olyanunk, hogy autó típus. Mit lehet tudni egy autóról? Fogyasztás, ülőhelyek száma, maximális sebesség, gyorsulás. Legyen egy absztrakt Car osztályunk, amelyik ezeket mind tudja tárolni, és visszaadni, és az egyes leszármazott osztályok számolják ki a konkrét értékeket a konstruktorukban. Ez logikusnak tűnik:

  1. Egyrészt minek írnánk meg minden egyes konkrét osztályban azt a rengeteg gettert.
  2. Felesleges az értékeket folyamatosan kiszámolni, valahányszor szükségünk van rá, tárolhatjuk az egyszer kiszámított értéket.
  3. Másrészt az ősosztály nem képes kiszámítani a konkrét értékeket, tehát azt a leszármazottakban kell.

A sok paraméter közül most vegyük a maximális sebességet, amúgy is csak az ami számít a tinédzserek életében, az a szekszi, meg az autóskártyában is a nagyobb sebesség üti a többi lapot. Hogy elférjen a pelenkázó a kombi csomagtartójában, az csak később jön. A maximális sebességgel is lesz problémánk éppen elég (naná, majd arról írok blogot, hogy nézd már, működik a hello world!), lesz traffipax NPE alakjában meg minden szép és jó, a többi paraméter meg nem sokat tenne hozzá, csak elvonná a tisztelt blog olvasó figyelmét a lényegről, ha eddig a locsogásommal még nem sikerült volna.

public abstract class Car {
    private final int maximumSpeed;
    abstract int calculateMaximumSpeed();
    public Car() {
        maximumSpeed = calculateMaximumSpeed();
    }
    public int getMaximumSpeed() {
        return maximumSpeed;
    }
}

Ez, azt hiszem még nem okozott nagy meglepetést. És akkor most jöjjön a Vw Golf GTI.

public class VwGolf extends Car {
    private final Boolean isGti;
    public VwGolf(boolean gti) {
        isGti = gti;
    }
    @Override
    int calculateMaximumSpeed() {
        final int maxSpeed;
        if (isGti) {
            maxSpeed = 246;
        } else {
            maxSpeed = 172;
        }
        return maxSpeed;
    }
}

Ebben sincs nagy varázslat. Ez egy VW Golf, ami lehet GTI vagy sima. Most 16 helyett csak két változatot modellezünk. Az isGti értéke a létrehozott objektumban vagy true, vagy false attól függően, hogy milyen értéket adtunk meg a konstruktorban, és ez nem is változhat az objektum élete során, hiszen a változó final.

Most jött el az a pillanat, amikor unit tesztelünk. A teszt osztályt nem másolom ide: semmi egyéb, mint egy VwGolf létrehozása new operátorral. Hanem a kimenet!

Exception in thread "main" java.lang.NullPointerException
	at example3.VwGolf.calculateMaximumSpeed(VwGolf.java:9)
	at example3.Car.<init>(Car.java:5)
	at example3.VwGolf.<init>(VwGolf.java:3)
	at example3.TestCar.main(TestCar.java:6)

A VwGolf.java 9-es sorában kaptunk NPE-t, ami azt jelenti, hogy akármennyire is lehetetlennek tűnik, az isGti változó értéke bizony null.

Ilyenkor jön a nemtörődöm megoldás, hogy legyen boolean a változó és akkor nem kaphatunk NPE-t.

Hogyan lehet null ez a változó, amikor értéket kap a konstruktorban, és utána úgy is marad? Talán segítheti a megértést, hogy hogyan kerültünk erre a sorra. A Car ötödik sorából jött a hívás, ami stimmel: ez a Car konstruktora. Oda pedig a VwGolf harmadik sorából jutottunk. Ez a konstruktor feje, még az előtt, hogy az isGti változó értéket kapott volna. Akkor pedig már értjük, hogy hogyan született az NPE. Azért ismételjük át, hogy mi történik egy objektum létrehozása közben:

  1. Meghívjuk a konstruktort, de az ahelyett, hogy elkezdene futni, meghívja az ős osztály konstruktorát.
  2. Amikor az ősosztály létrehozása lefutott, még mindig nem kezdi el végrehajtani a konstruktort, hanem nekiáll lefuttatni a dinamikus inicializáló részeket, abban a sorrendben, ahogy a kódban szerepelnek.
  3. Amikor ezek lefutottak, akkor lefut a konstruktor.

Természetesen az ősosztály konstruktorának a meghívása során is minden lefut az ősosztályban, aminek kell, meg annak az ősosztálya is, feltéve, hogy van az osztálynak ősosztálya.

Ugye mindenki tudja, hogy csak egy olyan osztály van, amelyiknek nincs ősosztálya, mert minden más osztálynak van ősosztálya, akkor is, hanem írjuk ki, hogy extends Object

Demonstrációként legyen itt két osztály:

public class Progenitor {
    private int one(String m) {
        System.out.println("Progenitor "+m);
        return 1;
    }
    final int var1 = one("variable initialization 1");
    {
        System.out.println("Progenitor dynamic initializer before construtor");
    }
    final int var2 = one("variable initialization 2");
    public Progenitor() {
        System.out.println("Progenitor Constructor");
    }
    final int var3 = one("variable initialization 3");
    {
        System.out.println("Progenitor dynamic initializer after construtor");
    }
    final int var4 = one("variable initialization 4");
}
public class Offspring extends Progenitor {
    private int one(String m) {
        System.out.println("Offspring "+m);
        return 1;
    }
    final int var1 = one("variable initialization 1");
    {
        System.out.println("Offspring dynamic initializer before construtor");
    }
    final int var2 = one("variable initialization 2");
    public Offspring() {
        System.out.println("Offspring Constructor");
    }
    final int var3 = one("variable initialization 3");
    {
        System.out.println("Offspring dynamic initializer after construtor");
    }
    final int var4 = one("variable initialization 4");
}

Aminek a kimenete, amikor egy new Offspring() elsül:

Progenitor variable initialization 1
Progenitor dynamic initializer before construtor
Progenitor variable initialization 2
Progenitor variable initialization 3
Progenitor dynamic initializer after construtor
Progenitor variable initialization 4
Progenitor Constructor
Offspring variable initialization 1
Offspring dynamic initializer before construtor
Offspring variable initialization 2
Offspring variable initialization 3
Offspring dynamic initializer after construtor
Offspring variable initialization 4
Offspring Constructor

Nincs ebben semmi varázslat, és akkor sem hazudtam, amikor azt írtam feljebb, hogy

“Az isGti értéke a létrehozott objektumban vagy true, vagy false attól függően, hogy milyen értéket adtunk meg a konstruktorban, és ez nem is változhat az objektum élete során, hiszen a változó final.”

csak éppen fontos kihangsúlyozni, hogy AZ OBJEKTUM ÉLETE SORÁN. Amikor pedig az NPE robban, akkor az objektumunk még nem él, még csak fetus, még nem csusszant ki a java szülőcsatornáján. Ilyenkor sok minden lehet. Például az is, hogy a final változók még nincsenek inicializálva.

Hogyan oldjuk meg ezt a problémát? Például úgy, hogy a konstruktor nem próbálja meg kiszámolni a maximális sebesség értékét, hanem hagyja, hogy ezt a getter tegye meg az első meghívásnál. Persze ilyenkor előjön a singleton problémához hasonló szinkronizáció. De az már egy másik blog bejegyzés.

Amúgy a JPL_Coding_Standard_Java ajánlás R15 pontja szerint ne hívjunk konstruktorból olyan metódust, amelyik nem privát vagy nem final, mert abból baj lehet. Itt lett is. Na de hát akkor hogyan?

Két megoldást fogok mutatni. Az első egy trükkös, összetett pattern, ami egy kicsit hasonlít a lazy singleton megoldáshoz, legalábbis az adja az ötletet. Ennek megfelelően bonyolult, és nehezen érthető. A második megoldás nem olyan szexi, egy egyszerű gyártó pattern. Ennek megfelelően egyszerűbben érthető, és ezért production kódban csak a második megoldást ajánlom. Az első csak okulásul.

Késleltetett inicializálás

Ugye a gond abból adódott, hogy amikor a Car konstruktora ki akarta számoltatni a végsebességet, akkor a leszármazott konstruktor még nem futott le, nem állt rendelkezésre az inicializált osztály. Csináljuk úgy, hogy legyen egy második, belső osztály a Car osztályon belül, amelyik az autó állapota:

public abstract class Car {
    protected class State {
        int maximumSpeed = 0;
        public State() {
            state = this;
            maximumSpeed = calculateMaximumSpeed();
        }
    };
    private State state;
    abstract int calculateMaximumSpeed();
    public int getMaximumSpeed() {
        return state.maximumSpeed;
    }
}

A State persze magától nem fog létrejönni, de megtehetjük azt, hogy a leszármazott osztály konstruktorában hozzuk létre:

public class VwGolf extends Car {
    protected class State extends Car.State {}
    final Boolean isGti;
    public VwGolf(boolean gti) {
        isGti = gti;
        new State();
    }
    @Override
    int calculateMaximumSpeed() {
        final int maxSpeed;
        if (isGti) {
            maxSpeed = 246;// km/h
        } else {
            maxSpeed = 172;// km/h
        }
        return maxSpeed;
    }
}

Amikor a new State() lefut, akkor az objektumunk már inicializálva lett, bár hivatalosan még nem élő objektum, hiszen a konstruktor még nem tért vissza. A papa osztályban lefut a State konstruktora, és jól el is teszi magát a körülvevő osztály state változójába: a leszármazottaknak ezzel sem kell foglalkozni. Csak annyi a dolguk, hogy létrehozzák a State objektumot, amikor már létre lehet hozni. Ez más működik.

Egyszerű ez a megoldás? Könnyen érthető? Nem. Ha te könnyen megértetted, az azt jelenti, hogy jóval az átlag programozó felett vagy. Ugyanakkor a kódot az átlag programozó fogja karbantartani, tehát KISS.

Gyártósor

Mert hogyan is gyártsunk autót másképp, mint gyártósoron.

public abstract class Car {
    int maximumSpeed = 0;
    abstract int calculateMaximumSpeed();
    public int getMaximumSpeed() {
        return maximumSpeed;
    }
    public void setMaximumSpeed(int maximumSpeed) {
        this.maximumSpeed = maximumSpeed;
    }
}
public class VwGolf extends Car {
    final Boolean isGti;
    public VwGolf(boolean gti) {
        isGti = gti;
    }
    @Override
    int calculateMaximumSpeed() {
        final int maxSpeed;
        if (isGti) {
            maxSpeed = 246;// km/h
        } else {
            maxSpeed = 172;// km/h
        }
        return maxSpeed;
    }
}
public class VwGolfFactory {
    public static VwGolf produceNewCar(boolean gti) {
        VwGolf carGolf = new VwGolf(gti);
        carGolf.setMaximumSpeed(carGolf.calculateMaximumSpeed());
        return carGolf;
    }
}

Egyszerű? IGEN. Érthető? IGEN. Karbantartható? IGEN. Akkor ezt. Az eredeti ötlet a cikkhez a JPL_Coding_Standard_Java dokumentumból jött, de egy “kicsit” ki lett dolgozva, mert kérés volt a cikk után, hogy ne csak olyanról írjak, amit akkor tanultam 🙂

4 responses to “Egy kis fejtörő, autós játék

  1. tornaia szeptember 11, 2013 10:56 de.

    1. A késleltetett inicializáláskor a VwGolf osztályban a “protected class State extends Car.State {}” nem feleseges?

    2. Az ilyet hibákat elkerülendő szerencsére van egy jó kis ConstructorCallsOverridableMethod nevű PMD szabály is, tehát még unit teszt sem kell ahhoz, hogy megfogjuk a hibát. Persze ha valaki nem ír tesztet és nem figyeli a kódelemzőt a commit után, akkor csak egy szigorúra állított build breaker plugin állít(hat)ja meg…

    • Peter Verhas szeptember 11, 2013 11:15 de.

      1. Persze felesleges, de csak technikailag. Nem hivatkozom clean code, meg olvashatóságra, mert egy antipattern esetében ez elég kérdéses lenne. De a példa egy leegyszerűsített struktúrát mutat, és arra utal, hogy a valóságban a State osztályban lehetnének saját változók, amik az ősben nincsenek.

      2. Igen, pontosan ezt mondja a hivatkozott JPL coding standard is. Javaslom az olvasását.

  2. MagyarL szeptember 11, 2013 2:18 du.

    A második (Gyártósor) megoldásban viszont a “setMaximumSpeed()”-al bárki átírhatja a “maximumSpeed” változó értékét, ami az első esetben nem volt lehetséges.

    Persze ha egy kis tuningot végeznek a fiatalok akkor mindenképpen kell ez a metódus 🙂

  3. tamasrev szeptember 17, 2013 8:33 du.

    A setMaximumSpeed lehetne egy default elérésű okosság, amit elsősorban csak a gyártó hívhat. És akkor az encapsulation-t is csak egy kicsit izéljük meg. Kivéve persze, ha pont ezt ajánlja a JPL – előbb-utóbb elolvasom azt is.

Vélemény, hozzászólás?

Adatok megadása vagy bejelentkezés valamelyik ikonnal:

WordPress.com Logo

Hozzászólhat a WordPress.com felhasználói fiók használatával. Kilépés / Módosítás )

Twitter kép

Hozzászólhat a Twitter felhasználói fiók használatával. Kilépés / Módosítás )

Facebook kép

Hozzászólhat a Facebook felhasználói fiók használatával. Kilépés / Módosítás )

Google+ kép

Hozzászólhat a Google+ felhasználói fiók használatával. Kilépés / Módosítás )

Kapcsolódás: %s

%d blogger ezt kedveli: