tifyty

pure Java, what else ?

Ninja implementálás, level interfész

Azt a felelőtlen ígéretet tettem az Interfész implementálás, level ninja című posztban, hogy hétvégén megírom, hogy pontosan mi is a megoldás, meg, hogy honnan jött össze ez az egész furcsa kérdés. Emlékeztetőnek, aki lusta visszalapozni, az volt a kérdés, hogy …

Hogyan fordulhat olyan elő, hogy egy osztályunk implementálni akar két interface-t, a két interface-ben nincsenek egymást “ütő” metódusok, a Java compiler mégsem engedi meg, hogy mind a két interface-t implementáljuk.

Hogy még egyszerűbb legyen a dolog: I1 és I2 interface-k egyike sem definiál egyetlen metódust sem, sem direk módon, sem pedig öröklődésen keresztül. A C osztály pedig csak ennyi:

public class C implements I1, I2{

}

És mégsem fordul le. Mi lehet?

Mielőtt belekezdenénk, néhány megjegyzés:

  • Az, hogy az interfészek egyike sem implementál metódusokat itt azt jelenti, hogy nem csak ők maguk nem implementálnak metódust, de egyetlen olyan interfész sem, amelyet bármelyik a példában szereplő interfész kiterjeszt, akár explicit, akár implicit módon.
  • Van olyan eset is, amikor két interfész azért nem implementálható egy osztályban, mert egymással nem kompatibilis metódusokat definiálnak. Ez azt jelenti, hogy az egyik interfészben van olyan szignatúrájú metódus, amelyik szignatúrával található metódus a másikban is, de a két metódus visszatérési típusa egymással nem kompatibilis. Nem kompatibilis, azaz nem létezik olyan osztály, amelyik mind akét típusnak leszármazottja lenne, és ezért nem lehet mind a két metódust egy metódusban implementálni az implemetáló osztályban. Mint McAllister megjegyezte:

Ha egy interfacenek van egy metódusa és annak egy visszatérési értéke, akkor az implementáló osztály amikor megvalósítja az interface metódusát használhat egy leszármazott típust a megvalósítandó metódus visszatérési értékének. Amúgy nem megy.

pl.

interface I3 { Object a();}
class B implements I3 { String a(); }

Daniel McAllister

Ez nagyon így van. Szóval lehet, hogy egy kicsit pongyolán fogalmaztam, de őszintén: a mostani megfogalmazás sem precíz, és nem is akarom, mert akkor inkább idemásolhatnám a java biblia megfelelő idézetét, például a 9.4.1.1 fejezetet, és együtt énekelhetnénk a hozzá illő zsoltárt. Ugye a Java programozó bibliája a jsl.

Szóval: hogyan kell olyan interfészeket gyártani, amiket ha implementálni akarok egy osztályban, akkor a fordító nem csak kiröhög, hogy próbálkozzam csak, úgysem fog menni, hanem egyenesen megtagadja még a lehetőséget is. Kofa elég gyorsan kitalálta (laza a projekt?), hogy ez csak úgy lehet, ha van egy közös generikus ősük, amit más konkrét típussal terjesztenek ki. Nah ez így eléggé kódolt, de ha kód, akkor legyen, így már érthetőbb lesz:

public interface S<T> {}
public interface I1 extends S<String> {}
public interface I2 extends S<Number> {}

Ez Kofa kommentjéből van. Miért nem lehet implementálni egyszerre I1 és I2 interfészeket?Hát azért mert …. (wait for it)… drámai dobpergés…

Előbb elmondom, hogy hogyan akadtam ebbe bele. Mert élő ember, legalábbis olyan aki nem hatoldalú szabályos test, nincs aki addig olvassa a szabványt, amíg meg nem világosodik neki, hogy ilyent nem lehet. A dolog egy kellemes(nek nem mondható, esős és hideg) nyári délelőtt jött elő, amikor éppen GWT GUI-t programozgattam a fűtött irodában (június, 2013). Mindenféle eseményeket kellett kezelni, Event-eket, ha így ismerősebb. Eseményeket lehetett dobni (még mindig jobb, mint kivételes törpéket), és amikor egy esemény sorra került az eseménybuszon, akkor meghívta az esemény kezelőjét. Ez esemény kezelője implementálta a kezelő interfész, amiben definiálva van egy metódus, aminek a neve általában onEseménytípus. Valahogy így:

interface HappyHandler extends EventHandler {
  public void onHappiness(HappyEvent event);
}

interface HasHappyEvents {
  public HandlerRegistration addHappyHandler(HappyHandler handler);
}
class HappyEvent extends AbstractEvent{
  public static AbstractEvent.Key KEY = new AbstractEvent.Key(){...}

  public GwtEvent.Key getKey(){
    return KEY; 
  }
  ...
}
class HappyEvent extends GwtEvent {
  static Key<HappyEvent,HappyHandler> KEY = new Key<HappyEvent,HappyHandler>(){
    protected void fire(HappyHandler handler, HappyEvent event) {
       handler.onHappiness(event);
    };
   ...
}

Nem vagyunk mi itten túl boldogok? Hányszor írtuk már le azt, hogy ‘happy’? Mi van itt, születésnap?

Na szóval: legyen az esemény neve akármi, amit akarunk, a boldogságot nem lehet megúszni, essünk túl rajta: HappyEvent. Egyszer csak le kell írnunk ezt a szót a kódban, de az ideális az lenne, ha nem kellene többet. A kezelő interfész legyen Handler. Eddig HappyEventHandler volt, de ha ez az interfész a HappyEvent osztály publikus nested interfésze, akkor ezentúl a neve Handler, ha meg kívülről kell valakinek (és persze fog neki), akkor HappyEvent.Handler. Ez kell implementálnia minden olyan osztálynak, amelyik ezt az eseményt kezelni szeretné. (Az ugye világos, hogy nested interface és nem inner interfész! Vigyázz, beugratós!)

Az sem szerencsés, hogy az eseményt kezelő metódus public void onHappiness(HappyEvent event);. Itt ugyan egy kicsit el van fedve a dolog, de igazából, ha konzekvensek vagyunk, akkor ez így nézne ki: public void onHappyEvent(HappyEvent event); Már megint túl sok a boldogság. Miért nem lehet egyszerűen public void on(HappyEvent event);. Aztán ha több eseménykezelőt implementálunk, akkor majd az argumentum megmondja, hogy melyikről van éppen szó. Ezért jó a túltöltés (overload). Ha pedig megállapodtunk abban, hogy minden eseménykezelő metódus neve on, akkor miért kell ezt minden Handler interfészben leírni? Miért nem lehet egy AbstractRidiculousSimpleSuperHandler<MyEvent> interfész, amit kiterjesztünk, megadva MyEvent-ként azt az eseményt, amelyiknek amúgy is a nested interfészét definiáljuk éppen.

Megérkeztünk? Ott vagyunk már? (Szamár!!!!)

Bizony meg, mert abban a pillanatban, amikor a refaktorálással idáig eljutottam, és implementálni akartam egy osztályban két eseményt a javac pofán vágott, hogy csak néztem, mint magas kilátó a google keresőre. Aztán rákerestem, gondolkodtam, és lassanként megvilágosodott, hogy miért is. (Dotnetesnek meg ne mutassad a cikket, mert itt fog ugrálni körülötted, hogy bezzeg a C# … És igaza van!)

Mert mi történik akkor ha van egy eseményem, amelyiket kezelni akarok? Az eseményhez be van jegyezve egy (vagy több) esemény kezelő objektum. Ebben kell meghívni az eseménykezelő interfész által definiált on metódust. De mi van akkor, ha a kezelő egy olyan metódust implementál, amelyik egy generikus szuper interfészből jön? Nézzük meg az egész példát, az egyszerűség kedvéért, ha valaki ki akarja copy/paste egy osztályba van minden belezsúfolva:

package genericSample;

public class DemoClass {
  interface AbstractRidiculousSimpleSuperHandler<MyEvent> {
    void on(MyEvent event);
  }

  static class Karacsony {
    interface Handler extends AbstractRidiculousSimpleSuperHandler<Karacsony> {
    }
  }

  static class Szulinap {
    // interface Handler extends AbstractRidiculousSimpleSuperHandler<Szulinap>
    // {
    interface Handler {
      void on(Szulinap event);
    }
  }

  static class AjandekotKapok implements Szulinap.Handler, Karacsony.Handler {

    @Override
    public void on(Karacsony event) {
      System.out.println("Karacsony van");
    }

    @Override
    public void on(Szulinap event) {
      System.out.println("Szulinap van");
    }

  }

  public static void main(String[] a) {
    Karacsony karacsony = new Karacsony();
    Szulinap szulinap = new Szulinap();
    AjandekotKapok kezelo = new AjandekotKapok();
    AbstractRidiculousSimpleSuperHandler kk = kezelo;
    kk.on(karacsony);
    kezelo.on(szulinap);
  }
}

Ha lehetne, hogy a szülinap is kiterjessze ugyanazt az interfészt (ahogy a kikommentezett részben van), akkor az on() metódus meghívásakor nem tudná a Java, hogy melyiket is kell meghívnia. Ugyanis kettő van, de ugyanarra az egy, a AbstractRidiculousSimpleSuperHandler interfészben definiált metódusra hivatkoznának. Amikor túllovaglás (override) van, akkor a futtató rendszer megnézi, hogy milyen osztályhoz tartozik az aktuális objektum. Az amelyikre a fenti példában a kezelo és a kk változó is hivatkozik. Tulajdonképpen a kk változó teljesen felesleges is, hiszen nem az az érdekes, hogy a változónak milyen a típusa, hanem az, hogy az objektumnak milyen a típusa: futási időben dől el, hogy melyik metódust fogja meghívni a JVM, és mindegy hogy az objektumra hivatkozó referenciát milyen típusú változóban tároltuk. Az objektum típusa pedig AjandekotKapok, ami lehet Szulinap.Handler, Karacsony.Handler vagy AbstractRidiculousSimpleSuperHandler. De, hogy éppen milyen generikus argumentummal, az bizony nincs benne az objektumban. Így egy on metódusból nem is lehet kettő az implementációban.

Mi akkor a megoldás? El kell dobni a közös generikus őst, és körmölni kell rogyásig, minden egyes interfészben leírva, hogy on(EseményTipus esemeny);? Gyenge nyelv lenne a Java, ha ez lenne a megoldás. Ehelyett az egyes kezelőket inner classokba (és nem nested) kell elhelyezni, és mindegyik csak egy kezelő interfészt implementál:

package genericSample;

public class DemoClass {
  interface AbstractRidiculousSimpleSuperHandler<MyEvent> {
    void on(MyEvent event);
  }

  static class Karacsony {
    interface Handler extends AbstractRidiculousSimpleSuperHandler<Karacsony> {
    }
  }

  static class Szulinap {
    interface Handler extends AbstractRidiculousSimpleSuperHandler<Szulinap> {
      void on(Szulinap event);
    }
  }

  static class AjandekotKapok {

    class K implements Karacsony.Handler {
      @Override
      public void on(Karacsony event) {
        System.out.println("Karacsony van");
      }
    }

    class S implements Szulinap.Handler {
      @Override
      public void on(Szulinap event) {
        System.out.println("Szulinap van");
      }
    }

    public final K k = new K();
    public final S s = new S();

  }

  public static void main(String[] a) {
    Karacsony karacsony = new Karacsony();
    Szulinap szulinap = new Szulinap();
    AjandekotKapok kezelo = new AjandekotKapok();
    kezelo.k.on(karacsony);
    kezelo.s.on(szulinap);
  }
}

Ezzel ugyan nehézkesebb a meghívás, hiszen nem elég azt leírni, hogy kezelo, hanem utána kell írni azt is, hogy melyik kezelő, pedig ezt lehetne tudni az esemény típusából is, viszont a jó hír, hogy ezt valójában, a konkrét példában nem kell. Az esemény kezelőket ugyanis nem a mi kódunk hívja meg, hanem a kertrendszer, így a k és s változókra sincs szükség. Az is előnye ennek a megoldásnak, hogy amíg a külső osztály maga volt az eseménykezelő, addig saját magát regisztrálta be a konstruktorból, paraméterként átadva a this értékét. Ez pedig az álmoskönyv szerint elég egészségtelen. Így viszont a frissen és ropogósan létrehozott inner class-okat regisztrálják be, így ezt az antipatternt is elkerüljük.

One response to “Ninja implementálás, level interfész

  1. Peter VerhasPeter Verhas június 11, 2013 10:28 de.

    On behalf of Lukas:

    Aha, I get it now. 🙂
    It’s easy to show an example why you shouldn’t be able to implement both I1
    and I2 in C. Assume you could do:

        class C implements I1, I2 {}
    

    Now write this perfectly legal method:

        public void <T> T method(InterfaceGeneric<T> argument) {
            // ...
        }
    

    What’s the return type of this call? Integer or String?

        method(new C());
    

    If InterfaceGeneric had a method using T (e.g. “T t();”), it would be even
    simpler to show an example why this kind of “diamond inheritance” doesn’t
    work. An authoritative section of the JLS specifying this can be found here
    in section 8.1.5:

    http://docs.oracle.com/javase/specs/jls/se7/html/jls-8.html#jls-8.1.5

    Or more precisely:

    http://docs.oracle.com/javase/specs/jls/se7/html/jls-8.html#d5e9820

    Cheers
    Lukas

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: