tifyty

pure Java, what else ?

Havi archívumok: szeptember 2013

Java Horror 1

Motto: Olyan dolgok a Java nyelvről, amelyekről nem tudtál, és azt kívánod, bár most se tudnád.

package Class;
public class Class extends Exception {
    int Class(Class klass) throws Class {
        if (klass.getClass() != Class.class) {
            throw new Class();
        } else {
            return Class;
        }
    }
    final int Class;
    Class() throws Class {
        Class = Class(this);
    }
}

És ez lefordul, és használható. Hogy mire?

Elrettentő példának.

Ott vagyunk már?

A Shreck film nem is tudom hanyadik része jutott a múltkor az eszembe, amikor is utaznak valahová, és a szamár állandóan kérdezgeti, hogy megérkeztek-e már. Akkor jutott eszembe, amikor egy projekt során egy feladatot megcsináltunk, majd kiderült, hogy bizony nincsen kész, mert még hiányzott, ez meg az, meg amaz. Csupa olyan dolog, amit teljes természetességgel kívánt meg a megrendelő (ebben az esetben nem cég ügyfél, csak projekt menedzsment), és igaza volt, csak éppen nem volt előre megmondva, hogy ezeket a dolgokat mind meg kell csinálni. Mert az természetes, hogy kell dokumentáció, csak persze nem mindegy, hogy milyen szintű.

Fontos, hogy legyen DoD, azaz definition of done (és nem védelmi minisztérium).

Az installáláshoz egy Java alkalmazásnál például elég lehet másfél oldal, amiből egy olyan rendszer gazda, akit joggal lehet annak nevezni, fel tudja installálni az alkalmazást, ha pedig valami nem működik, akkor telefonál, és segítünk. De lehet kívánni olyan dokumentációt is, amelyik lépésről lépésre leírja, hogy milyen parancsokat kell végrehajtani, mit hova kell másolni, mikor hova kell kattintani az egérrel. Azt is meg lehet kívánni, hogy ez a dokumentum a változó részeket, ami nem feltétlenül ugyan az minden környezetben, például direktori nevek, vagy application alias helytartóval (place holder) jelöljön, amiket a dokumentumban global search and replace módon lehet az aktuálisra kicserélni, így állítva elő a konkrét installálási utasítást, benne a megfelelő üres helyekkel, ahova a papíron kézzel írhatja be a rendszergazda, hogy pontosan mikor hajtotta végre azt a parancsot, kattintott, és mit tapasztalt, hogy pontos dokumentáció legyen az installációról.

A paceholder vs. helytartó fordításért elnézést kérek, egy réges régi projektnél így szerepelt a kiírásban, és nem tudtam ellenállni, hogy ide leírjam, hátha varázsol egy mosolyt a ráncos homlokok alá.

És nagyon nem mindegy, hogy mindez előre definiálva van-e, mert a másfél oldal megírása nagyon más erőforrást kíván, mind időben, mind minőségben, mind junior/senior kvalitásban, mint a legutolsó, legextrémebb példa. És ha nincs előre definiálva, hogy mit is kell pontosan megcsinálni, akkor amikor kiderül tisztázni kell azt is: ki fizeti a révészt, mert ha arra az obulusra számítunk, ami projekt cadaver nyelve alatt van, akkor már régen rossz.

Bizony nem egy olyan ügyfél van, aki rossz ösztönöktől vezérelve direkt, vagy csak tudat alatt nem tisztázza előre a pontos kívánalmait. A szállító is gondol valamit, meg a megrendelő is, és ez két dolog. A megrendelő pedig, amikor a projekt már fix áras akkor áll elő azzal, hogy ő pontosan hogyan is gondolta. Sok megrendelő ezt tudatosan űzi, és arra számít, hogy majd jól, gyorsan és olcsón kap mindent. Sőt azt is gondolja, hogy ha nem így tenne, akkor túlfizetné a szállítót, és borsos árat perkálna valami olyasmiért, ami nem ér annyit. És persze ezt sem az ujjából szopja, hanem a tapasztalataiból. Ez pedig azoknak a fejlesztőknek és csapatoknak a hibája, akik szembekerülve egy tapasztalatlan, és erőtlen ügyféllel kihasználják a helyzetet. Vagy csak ők is tapasztalatlanok, mint az ügyfél, és sokszor az első alkalom emléke, amikor a két fiatal ügyetlenkedik egész életre meghatározó. És amikor az ügyfél az egyetlen olyan módon akarja, ahogy a legtapasztaltabb prosti sem tudja (gy.k: ingyen) akkor van az, hogy az ügyfél képviselője, miközben ég a bőr a képéről, kénytelen azt mondani: “A főnökeim véleménye az…”, mert már a saját nevében nincs becsülete kimondani, hogy a róka csontvázáról még mennyi szőr és bőr kellene. (Ezt egyébként régi barátom szájából hallottam, akivel az élet során a szállító-megrendelői asztal hol egyik, hol meg a másik oldalán ültünk.)

Aztán ott a másik véglet, amikor a sprint vége felé közeledve egyszer csak felgyorsult a csapat, és egymás után lökte ki magából a feature-öket. Hát kicsit több volt a bug, a bug fix meg már a következő sprintre lett betervezve. Aztán belenéztem a kódba és leestem az asztal alá: majdnem üres osztályok, metódusok. Kész vannak? Ja, csak egy “kicsit” bugosak. És ezen, ha ilyen a csapat hozzáállása az sem segít ha megköveteljük a unit teszteket, és a kód lefedettséget, mert a kódlefedettség, meg a funkcionális lefedettség az nem ugyanaz, és ilyen esetben a különbség nem elhanyagolható. Azt a curry szószos mindenit, gondoltam magamban, pedig nem is funkcionális nyelven programoztak a kollégák. (Nem árulhatom el, hogy milyen nemzetiségűek voltak.)

Mostanában kicsit rendezettebb körülmények között dolgozom, nem is kicsit, de azért akármilyen a környezet, csodákkal, kód review során, mindig lehet találkozni.

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 🙂

Használjunk assertion-t

A JPL_Coding_Standard_Java (google a barátod) R21 szerint a programunkban használjunk assertion-öket. Nem szoktunk. Tegye fel a kezét, aki szokott. Most az az egy hazudós meg ott hátul, tegye le a kezét, úgyse hisszük el.

Miért nem használjuk? Mert általában nem fordul bele a kódba. Ahhoz, hogy beleforduljon külön meg kell kérnünk a fordítót, és a release meg a test kód fordítása közötti különbséget nem szokták támogatni a fordítás menedzselők. Mert általában futtatás során ki van kapcsolva a végrehajtásuk. (java -ea) Persze a JUNIT teszteknél élnek az assert utasítások, de amikor magasabb szintű tesztelést végzünk, ad-hoc, esetleg selenium-mal vagy valami mással az egész alkalmazást támadjuk, akkor általában már nem.

Ezért a fenti standard (vannak benne érdekes dolgok, lesz belőle még blogbejegyzés), azt mondja, hogy érdemes egy kis saját Assert osztályt létrehozni:

public class Assert {
  public static final boolean CONTRACT = true;
  public static void preCondition(boolean b) {
    if (CONTRACT && !b) {
      // naplózzuk az eseményt, esetleg kivételt dobunk
    }
  public static void postCondition(boolean b) {
    if (CONTRACT && !b) {
      // naplózzuk az eseményt, esetleg kivételt dobunk
    }
  }
}

Ez a kód pedig emígyen használandó utána:

import static contract.Contract.*;
class MyMathClass {
  double sqrt(double x) {
    if (CONTRACT) preCondition(x >= 0);
    double result;
      ... // itt számoljuk ki a négyzetgyököt a speciális szuper új algoritmusunkkal
    result = ...;
    if (CONTRACT) postCondition(result >= 0 && Math.abs((result * result) - x) <= EPSILON);
    return result;
  }
}

Mi történik itt, mit kell észrevenni?

Ha a kódot lefordítjuk, akkor a preCondition és a postCondition metódusok meghívásra kerülnek, és teszik a dolgukat. Ha ellenben átírjuk a CONTRACT változó értékét false-ra, akkor a Java fordító valamelyik lépésben kitörli az összes olyan kódot, ami az Assert utility osztály statikus metódusait hívja. Mivel mindegyik előtt ott van az if utasítás, ezért ez dead-code-dá válik. Ilyenkor már nem akarjuk ellenőrizni azokat a feltételeket (pre és post) amelyek a tesztelés alapján amúgy mindig rendben vannak. Ezzel az éles rendszert nem lassítjuk. Ha pedig valahol véletlenül mégis bent maradt egy hívás, mert pl. a felelőtlen programozó nem írta a hívás elé az if(CONTRACT) utasítást, akkor sem történik tragédia, hiszen mivel CONTRACT értéke hamis, az ellenőrzés nem történik meg. Egyedül az Assert osztály marad benne feleslegesen a JAR-ban, ezt meg ki lehet szedetni például a yGUARD-dal.

Tetszik? Hát, ízlések, és pofonok különbözőek. Mindenesetre az ajánlás elég komoly helyről jön, még annak ellenére is, hogy a mintakód az R28 ajánlásuk ellen való, amelyik azt mondja, hogy mindig használjunk { és } zárójeleket az if után. Persze ez pont nem az a hely, ahol ez jól mutatna, és nem is az, ahol akár olvashatóságban, akár egyébben gondot okozna.

Ebből már azért érezhető, hogy nem vagyok elragadtatva a konstrukcióért. Persze működik, meg jó, meg minden, de emlékeztet a régi C programozásra, amikor nekem kellett csomó mindent megcsinálnom (pl. memória kezelés), és nem a nyelv volt értem, hanem fordítva. Nektek?