tifyty

pure Java, what else ?

Havi archívumok: március 2014

Agilis, nem tervez!

Volt már erről szó, de most hallottam egy jó hasonlatot. Egy projektről mondta valaki, hogy olyan szintre fejlesztették a nem-tervezést, hogy építkezési hasonlattal élve ásattak velük egy gödröt 12 sprint alatt, majd az utolsó, 13. sprintben át kellett volna vinni a másik telekre. (A nyarakról nem is beszélve.)

A Java8 default metódus hazugsága

Mi az a default metódus?

A Java 8 megjelenésével lehetővé vált, hogy egy interfészt olyan módon módosítsunk új metódus beszúrásával, hogy az közben kompatibilis maradjon az őt implementáló osztályokkal. A Java 8 előtt ez nem volt lehetséges, mert ha egy új metódus került az interfészbe, akkor azt minden olyan osztálynak implementálnia kellett, amelyik az interfészt implementálta és maga nem volt absztrakt. Így ha egy interfészbe új metódus került egy library-ban, akkor csak úgy lehetett az új verzióra áttérni, hogy minden ilyen változást követni kellett a library-t használó kódokban. Ha tehát lett egy computeIfAbsent() metódus az interfészben, akkor azt az alkalmazásban az osztálynak is implementálnia kellett, még akkor is, ha semmit nem csinált vele, nem is használta semmilyen kód.

A Java 8 megjelenésével ez már nem szükséges. És mégis

Ha egy interfészhez egy default metódust adunk hozzá néhány, az interfészt implementáló osztály használhatatlanná válhat.

Először nézzük meg a default metódusok finomságait.

A Java 8-ban lehetőségünk van arra, hogy egy metódust az interfészben implementáljunk. (Statikus metódusok is lehetnek interfészekben, de ez most egy másik történet.) Azt a metódust, amit egy interfészben implementálunk ‘default’ metódusnak hívunk, és a default módosítóval jelöljük meg, jelezve, hogy nem absztrakt. (Amúgy maga a tény, hogy van törzse, és az argumentumot záró zárójele után nem pontosvessző jön eléggé kifejezi, hogy default metódussal van dolgunk, de a Java mindig is redundáns volt kicsit az olvashatóság kedvéért.) Az osztályok, amelyek implementálják ezt az interfészt öröklik a default metódusok megvalósítását is. Ha az osztálynak másképp van szüksége a metódusra semmi nem áll útjában, hogy felüldefiniálja (interfészben metódus nem lehet final).

Ha tehát van egy osztályunk, amelyik implementál egy interfészt, és ebbe az interfészbe belekerül később egy új default metódus az osztályunk továbbra is működőképes.

Többszörös öröklődés?

A dolog akkor kezd bonyolódni amikor a többszörös öröklődés kerül képbe. A Java mindig tartotta magát, hogy többszörös öröklődés csak definíciós szinten van, implementáció esetén nem lehet többszörös öröklődés (ellentétben a C++-szal, ahol van, és elég nagy görcsölés.) Mi történik akkor, ha egy osztály több (legyen az egyszerűség kedvéért kető) interface-t is implementál amelyek mindegyikében megtalálható az m() metódus. Ha mindegyik interface-ben abstrakt a metódus, akkor úgy mint az eddigi Java verziókban az osztálynak implementálnia kell a metódust. Ebben nincs változás. De mi van akkor ha pontosan egy interfész ad default implementációt? Nos a Java 8 válasza az, hogy ebben az esetben is implementálnia kell az osztálynak a metódust, különben nem fog lefordulni. Ezek után mondani sem kell, hogy ha több default implementációt is örököl az osztály, akkor ugyanez a helyzet.

Megjegyzés: Ha az interfészek a default implementációt úgy adják, hogy öröklődésen keresztül végül is ugyanaz az egy default implementáció jelenik meg az osztályban örököltként, akkor az nem probléma. Azaz ha az interfész A implementálja m()-t, majd B és C interfészek kiterjesztik A-t és a D osztály implementálja B és C-t, akkor örökli az m() implementációját is. Úgy érdemes fogalmazni, hogy egy osztály pontosan egy default implementációt tud örökölni, akár több leszármazási úton keresztül de csak akkor ha ugyanezt a metódust nem örökli absztaktként is.

Természetesen ha az osztálynak implementálnia kell a metódust, akkor az történhet magában az osztályban, de örökölheti az implementációt kiterjesztett osztályból is.

Még egy megjegyzés: Bármennyire is vonzó lehetne default implementációt adni például a toString() metódusra: nem lehet. Ami az Object-ből öröklődik, az érinthetetlen. És ez jobb így.

No de akkor most térjünk vissza az előző heti rövid poszthoz

Ha egy C osztály implementál egy I interfészt, és I-ben van egy absztrakt metódus, amit C nem implementál, akkor C nem fordul le, de futni azért még futhat.

valamint amit az előbb írtam, és amin esetleg átsiklott a tisztelt olvasó: “…ebben az esetben is implementálnia kell az osztálynak a metódust, különben nem fog lefordulni…“.

Nem fog lefordulni. De futni fog? Mondjuk ha már korábban lefordult olyan interfész verziókkal, amelyek nem okoztak gondot a fordítónak. A válasz az, hogy fog futni. Vagy nem. A Java 8 nem igazán konzisztens ebben az esetben. Amúgy meg van az oka, de ebbe nem érdemes belemenni, mert a release kijött, és mindenkinek meg volt a lehetősége beleszólni korábban. Ha pedig nem tette, most már késő. (Én sem tettem, de nem is értek annyira a Java-hoz meg a programozáshoz, hogy meg mertem volna tenni.)

  • Mondjuk van két interfészünk, és egy osztály, amelyik implementálja mind a két interfészt.
  • Az egyik interfész implementálja a default metódust m().
  • Az osztály és az interfészek lefordulnak.
  • Változtassuk meg az interfészt, amelyik nem tartalmazta eddig az m() metódust, hogy deklarálja azt absztraktként.
  • Fordítsuk le csak a módosított interfészt.
  • Futtassuk az osztályt.

default method multiple inheritance
Ebben az esetben az osztály fut. Nem tudjuk újra fordítani a módosított interfésszel, de mivel a régivel már le lett fordítva: fut. Most

  • módosítsuk az interfészt amelyikben az absztrakt metódus van, és készítsünk az m() metódushoz egy default implementációt.
  • Fordítsuk le csak a módosított interfészt.
  • Futtassuk az osztály: hiba!

Ha két olyan interfész is van az öröklődési láncban, amelyik default implementációt ad egy metódusra, akkor az implementáló osztályban nem lehet meghívni a metódust, ha az nincs explicite az osztályban vagy egy ősosztályban definiálva.

invalid multiple inharitance of default methods
Ugyanakkor a lefordított .class továbbra is kompatibilis. Be lehet tölteni, és el lehet indítani a futtatását mindaddig, míg nem történik hívás arra a metódusra, amelyet többszörösen definiálnak az interfészek.

Minta kód

directory structure of test

A fentiek demonstrálására készítettem, egy test könyvtárat a C.java osztály számára, és három alkönyvtárat a két I1.java és I2.java interfészek különböző verzióihoz. A base könyvtár tartalmazza azt az interfész verziót, amelyik jó a fordításhoz is, és a futtatáshoz is. I1 tartalmazza az m() metódust default implementációval. Az interfész I2 ebben a változatban nem tartalmaz semmilyen metódust.

Az osztály tartalmaz egy main metódust, így futtatható egyszerűen a tesztünk során. A teszt ellenőrzi, hogy van-e valamilyen parancssori argumentum, így később meg tudjuk hívni úgy is, hogy meghívja az m() metódust, és úgy is, hogy ne.

~/github/test$ cat C.java 
public class C implements I1, I2 {
  public static void main(String[] args) {
    C c = new C();
    if( args.length == 0 ){
      c.m();
    }
  }
}
~/github/test$ cat base/I1.java 
public interface I1 {
  default void m(){
    System.out.println("hello interface 1");
  }	
}
~/github/test$ cat base/I2.java 
public interface I2 {
}

Ezeket a programokat le tudjuk fordítani, és le is tudjuk futtatni:

~/github/test$ javac -cp .:base C.java
~/github/test$ java -cp .:base C
hello interface 1

A compatible könyvtár egy olyan verziót tartalmaz az I2 interfészből, amelyik deklarálja de nem implementálja az m() metódust. Az I1.java tartalma mindegyik könyvtárban ugyanaz.

~/github/test$ cat compatible/I2.java 

public interface I2 {
  void m();
}

This can not be used to compile the class C:

~/github/test$ javac -cp .:compatible C.java 
C.java:1: error: C is not abstract and does not override abstract method m() in I2
public class C implements I1, I2 {
       ^
1 error

A hibaüzenet nagyon precíz. Mivel van egy C.class fájlunk az előző fordításból ha lefordítjuk csak az interfészeket a compatible könyvtárban, akkor még mindig tudjuk futtatni az osztályt:

~/github/test$ javac compatible/I*.java
~/github/test$ java -cp .:compatible C
hello interface 1

A harmadik wrong nevű könyvtár az I2 olyan verzióját tartalmazza, amelyik definiálja az m() metódust:

~/github/test$ cat wrong/I2.java 
public interface I2 {
  default void m(){
    System.out.println("hello interface 2");
  }
}

Azt gondolom, hogy a C osztályt ezekkel az interfészekkel lefordítani megpróbálni sem kell. De ha lefordítjuk az interfészeket, és elindítjuk a kódot az fut mindaddig, míg meg nem akarja hívni a m() metódust. Ennek demonstrálására használjuk a parancssori argumentumot:

~/github/test$ javac wrong/*.java
~/github/test$ java -cp .:wrong C
Exception in thread "main" java.lang.IncompatibleClassChangeError: Conflicting default methods: I1.m I2.m
	at C.m(C.java)
	at C.main(C.java:5)
~/github/test$ java -cp .:wrong C x
~/github/test$

Összefoglalás

Ha most kezded el, kedves olvasó, Java 8 kompatibilisíteni a könyvtáradat, és default metódusokat adsz az interfészekhez, akkor valószínűleg ez nem fog problémát okozni. Legalábbis ebben reménykedtek a JDK8 fejlesztő is, amikor funkcionális metódusokat adtak például a gyűjtemény interfészekhez. Az alkalmazások a Java 7 vagy korábbi implementációkon nyugszanak, és ezekben nincsenek default metódusok, így ha nem akarják újrafordítani az alkalmazásokat, akkor futni fognak még ütközés esetén is. De ha több olyan library is van, amelyik már Java 8 “kompatibilissé” lett téve, akkor azért van esély, hogy két metódus neve, szignatúrája ugyanaz lesz. Hogyan kell elkerülni?

Tervezd meg a library API-t előre. Ne számíts rá, hogy majd a default metódus megoldja később a gondokat ha ki kell egészíteni az API-t. A default metódusok csak az utolsó menedék. Gondosan válaszd meg a metódus neveket. Aztán majd idővel megtanuljuk, hogy a default metódusokat hogyan lehet és érdemes használni Java-ban.

Java 8 teszt

Az új nyolcas java-ban sok minden egyéb mellett a következőt lehet állítani:

Ha egy C osztály implementál egy I interfészt, és I-ben van egy absztrakt metódus, amit C nem implementál, akkor C nem fordul le, de futni azért még futhat.

Az állítás eleje igaz volt eddig is, de a kövérrel szedett rész az új. Ha lesz időm a hétvégén kipróbálom, és megírom, hogy hogyan is kell ezt érteni. Addig is lehet kommentekben tippelni.

Egájl Eszti Máson

Megterveztünk egy projektet. Jól megbecsültük, hogy mennyi energia, ráfordítás, óra, cukor, liszt stb kell a kakaós csiga elkészítéséhez. Aztán megterveztük részletesen, hogy pontosan hogyan is kellene tekerednie, mikor kerüljön bele a cukor, a liszt, és milyen irányba forogjon a keverőgép tárcsája. Aztán kiderült, hogy cukorbetegek is fogyasztani fogják a kakaós csigát, valamint gurtnik és gurmék ezért nem jó bele akármilyen kakaó.

És akkor nekiálltunk, és átterveztük, hogy gesztenye és durum liszt keveréke legyen az alapanyag, és xilit, aspartám és szarharin legyen cukor helyett. Igaz, ilyenkor más a tészta állaga, más hőfokú sütő kell, és más a trutymó viszkozitása is, ezért másmilyen keverőgépet kellett rendelni. Szerencsére a régi keverőgépet kvázi költségmentesen el lehetett dobni, az új meg a fejlesztés idejére amúgy is csak egy VM.

Közben az eredeti tervek szerint már nagyban keverni kellett volna a tésztát, darálni a krumplit és reszelni a sült húst. De tervezés nélkül nem lehet nekiugrani egy projektnek sem, akkor sem ha akármilyen agilisak vagyunk. Valami fogalmunk kell, hogy legyen arról, hogy mit is fogunk a végén kakaós csiga néven kipréselni magunkból. De nyugodtak voltunk, mert tudtuk, hogy az eredeti becslések elég konzervatívak voltak, és készítettünk már hasonlót, ha nem is kakaós csigát, csak éppen fahéjas kígyót, meg vasabival fűszerezett birkapéniszt. Ez is csak ugyanolyan csemege, mint amilyent az ügyfelek szeretnek. Ugyan nem mindig érthető, hogy miért, de nem is az a dolgunk, hogy megértsük, igaz-e? (Nem.)

A projekt menedzsment azonban nem volt ilyen nyugodt, mert a számok azt mutatták, hogy a kakaós csiga nem lesz kész karácsonyra, amikor is piacon kell lenni a vásáron, különben minek az egész. Így aztán az egyik hajnali merevedés során (gy.k. a standup meeting-re gondolok) elhangzott a következő mondat:

Újra kell esztimálni az effortot úgy, hogy a kakaós csiga rilízelhető legyen karácsonyig.

Nincs ezzel semmi gond. Megcsinálod fiam, és kész! Mert ha nem, akkor … na akkor mi lesz? Mivan, mivan, mivan? Nem kell szembenézni a realitással, ki kell adni a parancsot, és akkor meg lesz! Ilyen egyszerű. Ez a projekt menedzsment. Kihozni az emberekből ami bennük van! Hogy mi van az emberekben, arról meg a projek menedzsernek sok éves a tapasztalata, bár talán ha egy sebészt, vagy proktológust megkérdezett volna, pontosabb ismeretei lennének.

De nem baj. Becsülünk, vagy kicsülünk, mi itt együtt csücsülünk, azt majd mondunk valamit, hogy meddig tart a feladat. Konzervatívan becslünk, hogy legyen tartalék akkorra amikor a projekt menedzsment majd benyomja a turbo búsztot, a projekt menedzsment meg tudja, hogy van tartalék, hiszen mindig is volt, hiszen a programozó, fejlesztő egy lusta állat, mindig alulbecsül. És így éldegélünk egymással szórakozva, ismerve a másikat, és ha néha nem becsülünk elég nagyot, akkor elcsúszik a projekt, és olyankor balhé van.

Így kell ezt professzionálisan csinálni?

Díszklémer: (olyan klémer ami semmire se jó, de szép) A fenti történet nem a mi pékségünkben történt, és bármi hasonlóság élő, halott, élőhalott, még meg sem született vagy bármilyen más személyekkel, tárgyakkal, emberek tárgyakkal, tárgyak tárgyakkal… szóval nem valószínű.

Akit meg részletesebben érdekel, az olvassa el a Clean Coder című könyvet.