Een methode met variabel return type

 
08 september 2010

In het nieuwe Java EE project waar ik op werk (ik houd dit keer geen dagboek bij, dus je hebt niets gemist) kwamen we onlangs in de situatie dat we eigenlijk een methode wilden hebben die afhankelijk van zijn argument een ander return type had. Volgens mij zou zoiets een welkome uitbreiding op talen als Java of C# kunnen zijn.

We hadden namelijk een aantal Stateless session beans die als service fungeerden om het domein te ontsluiten. Om ze aan te duiden had ik een enum gedefinieerd met voor elke service een constante: bijvoorbeeld Service.USERACCOUNTS, Service.ORDERS, enz. Dat bleek erg handig: niet alleen krijg je bijvoorbeeld gratis code completion, maar we gebruiken ze ook om mbv een custom annotatie (Attribute voor .Netters) aan te geven welke services je voor een bepaalde servlet geïnitialiseerd (althans, opgezocht in JNDI) wil hebben: @UsesServices({Service.USERACCOUNTS, Service.ORDERS}); en er zijn vast nog veel meer nuttige dingen te bedenken in andere situaties. In Java is het ook mogelijk om aan een enum direct functionaliteit te hangen, waarbij elke constante met een eigen setje parameters kan worden geïnitialiseerd. We konden dus ook meteen per constante de bijbehorende Service-Interfaceklasse eraan vasthangen en die met een getter ontsluiten. Daarmee hoefden we ons ook niet meer af te vragen of je nou de Local, de Remote, of de gezamenlijke super-interface moet opzoeken.

Maar als je je service bean eenmaal in JNDI hebt opgezocht, hoe sla je die referentie dan op? De services hebben allemaal een andere interface. Dus dan zijn er drie mogelijkheden. De eerste is om voor elke service in de gezamenlijke servlet-superklasse een field te definiëren en ze daarin op te slaan. Dat ziet er dan ongeveer zo uit:

protected void initServices() {
  for (Service service : getUsedServices()) {
    Object bean = lookup(service);
    switch(service) {
      case Service.USERACCOUNTS:
        userAccountsService = (UserAccountsService) bean;
        break;
      case Service.ORDERS:
        ordersService = (OrdersService) bean;
        break;
      // meer gevallen, een stuk of 13
    }
  }
}

Niet echt ideaal dus, zo’n for/switch-constructie. Bovendien heb je in je Servletklasse zelf dus allerlei variabelen rondslingeren die wel een bepaald type hebben en dus een specifieke service impliceren, maar gewoon null bevatten. Je snapt al wel dat we al menig NullPointerException hebben zien voorbijkomen.

De tweede mogelijkheid is om alle services een dummy super-interface IServiceBean te laten extenden, en ze vervolgens in een HashMap op te slaan: dus

protected void initServices() {
  serviceMap = new HashMap();
  for (Service service : getUsedServices()) {
    serviceMap.put(service, lookup(service));
  }
}

Het nadeel van deze constructie is dat overal waar je je service dan wil gebruiken, je de referentie eerst weer van IServiceBean naar zijn juiste interface moet typecasten voordat je de service-specifieke methodes kan aanroepen. Je weet vantevoren al wel dat dat zoveel werk is dat we er niet eens aan zijn begonnen.

De derde mogelijkheid is om elke servlet zijn eigen initialisatiemethode te laten krijgen. Dat resulteert in een onzalige berg gedupliceerde code, dus ook niet erg netjes (en de reden waarom we dit naar de superklasse hebben verhuisd), maar heeft wel als voordeel dat je nooit per ongeluk een nog niet geïnitialiseerde service wil aanspreken, omdat er daar dan geen referentie voor is, terwijl dat in de bovenstaande twee gevallen wel kan en mag volgens de compiler maar je dan op runtime een lekkere exception om je oren krijgt.

Maar wat je eigenlijk wil zeggen is gewoon:

getService(Service.USERACCOUNTS).registerUser(userDto);
// ...
getService(Service.ORDERS).deleteOrder(orderId);

en dat getService zich dan druk maakt over (liefst lazy en cached) initialisatie van de services, en je geen extra variabelen in je context hoeft te hebben om die services op te slaan. Maar ja, zo’n methode mag niet van de Java-compiler, omdat getService() in het ene geval een UserAccountsService-object moet teruggeven en in het andere in OrdersService.

Toch zou zo’n constructie in bepaalde gevallen wel mogelijk moeten kunnen zijn. De compiler controleert op sommige andere plekken ook al dat een waarde op een bepaalde plaats een compile-time constante moet zijn: bijvoorbeeld in een switch/case-constructie of in annotaties:

@MyAnnotation("some string literal")  // OK
@MyAnnotation(error.toString())  //FOUT ondanks dat 
                             // obj een constante is
public class MyClass {
  public final Object error = "not allowed";
  public void test(int y) {
    int x=7;
    switch(y) {
      case 0: log.debug("OK"); break;
      case x: log.debug("ERROR"); break;
    }
  }
}

Als we dus tegen de compiler kunnen zeggen dat we als argument alleen compile-time enum-constanten verwachten, kunnen we op compile-time ook bepalen wat het return type van de methode moet zijn. De bovenstaande getService(...) constructie wordt daarmee mogelijk.

Bovenstaande is niet helemaal nieuw: binnen de context van zg. Pure Type Systems is het ook mogelijk om typen afhankelijk te maken van objecten (om maar in Java-termen te blijven; het toverwoord is eigenlijk dat de de Pi-formatie-regel (*,[],[]) [star, box, box] in je axioma’s moet hebben). Pure Type Systems zitten echter veel meer in de hoek van de functionele programmeertalen waarin er geen veranderende state is, en al je variabelen dus in feite constanten zijn. Dat maakt de zaak niet direct geheel toepasbaar op talen als Java of C#, en moeten we trucs uithalen zoals dat we moeten kunnen aangeven dat er alleen compile-time constanten als argument meegegeven mogen worden.


Werken met ?
Kijk dan bij onze mogelijkheden voor starters en/of ervaren IT'ers.


Categorieën: Development


Reacties (15)

  • Jasper Stein schreef:

    @Jaap,
    Ik ken Spring zelf niet, maar wat ik erover lees zou dat een goeie oplossing kunnen zijn, kan ik me voorstellen. Toen ik op dit project kwam was er echter al een keuze gemaakt voor EJB, en ik begreep dat Spring en EJB niet zo heel jofel samengaan…? (In elk geval dat Spring het ‘lichte, simpele’ alternatief zou moeten zijn voor EJB…?)

    Geplaatst op 09 september 2010 om 9:23 Permalink

  • Jasper Stein schreef:

    @David,
    Ik weet niet of er vishaken zijn weggevallen, dat gebeurt weleens; bedoelde je getService<OrdersService>().deleteOrder(id)?

    Hoe dan ook, de methode getService is niet volledig generiek omdat je niet elk type wil kunnen opzoeken als service (getService<String>()???) Hoewel ik me wel kan voorstellen dat er iets kan lukken als je je generieke typevariabele inperkt icm zo’n dummy IServiceBean interface (dus in C# iets als getService<T>() where T : IServiceBean). Of dat in Java ook zo werkt weet ik niet helemaal zeker, het is alweer even geleden dat ik generics in Java uitgebreid genoeg heb bestudeerd om hier direct antwoord op te geven; ik weet wel dat hier C# en Java net even wat verder uit elkaar liggen dan normaal.

    Ik geloof trouwens dat die vishaken in C# optioneel zijn (en ze dus misschien niet zijn weggevallen) – maar in dat geval zou het me verbazen als de C#-compiler slim genoeg is om dit goed te keuren; immers de ene keer zeg je getService().deleteOrder(id) en daarna getService().registerUser(user) zonder dat die twee methoden in een gezamenlijke interface zitten. Dat lijkt dan meer op dynamic types; en dat hebben we zeker niet in Java…

    Geplaatst op 09 september 2010 om 9:19 Permalink

    • Davld schreef:

      De haken waren inderdaad weggevallen.. en j kan die haken alleen weglaten als je een argument meegeeft van het zelfde type (voor zover ik weet) anders weet de compiler absoluut niet wat ie moet doen…

      Geplaatst op 09 september 2010 om 9:34 Permalink

      • Matthijs schreef:

        Klopt dat geld niet voor returntypes

        Geplaatst op 09 september 2010 om 9:43 Permalink

    • Matthijs schreef:

      In c# kun je voorwaarde stellen aan de types die tussen mogen staan en de haken zijn dan optioneel zolang er geen abiguity ontstaat

      Geplaatst op 09 september 2010 om 9:21 Permalink

      • Jasper Stein schreef:

        @Matthijs,
        Ja, maar is het niet zo dat als je zegt getService<T>() where T:IServiceBean, dat je dan alleen methoden mag aanroepen die in IServiceBean worden gedefinieerd? Maar IServiceBean zelf bevat geen methoden dus daar heb je hier niets aan. En als je methoden gebruikt van +buiten+ IServiceBean, hoe weet de compiler dan in welk type hij moet zoeken?

        Geplaatst op 09 september 2010 om 9:29 Permalink

        • Davld schreef:

          als je getService<T>() where T : IServiceBean hebt en getService<OrderService>() doet, geeft ie gewoon een OrderService terug en geen IServiceBean… die restrictie zecht alleen dat er IServiceBean types in moeten.

          Geplaatst op 09 september 2010 om 9:38 Permalink

          • Matthijs schreef:

            Je moet dan echter wel nog hard casten naar de goede service anders ben je gebonden aan de methodes in IServiceBean

            Geplaatst op 09 september 2010 om 12:36 Permalink

          • Jasper Stein schreef:

            Ja, dat is ook wel een mooie oplossing, en heeft als voordeel dat latere services direct inpasbaar zijn zonder dat je je enum moet aanpassen. Nadeel is wel dat je nu moet onthouden welke generieke typeparameter je moet gebruiken – de @Local interface, de @Remote interface, hun gezamenlijke superinterface, of de implementatie zelf. Die info zit nu weggestopt in onze enum. Daarnaast moet je nog iets regelen voor registratie van de service bij JNDI – dat gebeurt nu ook mbv de enum. Maar dat is waarschijnlijk wel weer op andere, voor deze discussie niet zo relevante manieren op te lossen.

            In elk geval bedankt voor je reactie/input!

            Geplaatst op 09 september 2010 om 11:09 Permalink

  • Matthijs schreef:

    Ah ok, ik snap het probleem.
    Dan zouden generic methods in principe moeten voldoen

    Geplaatst op 09 september 2010 om 9:13 Permalink

  • Jasper Stein schreef:

    @Matthijs,
    Ik neem aan dat je een Singleton bedoelt voor elke service (dus bijv. OrdersService.getInstance())…? Nee, dat is niet toereikend. In Java EE gebruik je Stateless session beans omdat die door de EJB-container gemanaged worden; er kunnen er meerdere van zijn, en je hebt niet zelf in de hand hoeveel en wanneer er extra instanties aangemaakt worden. Dat regelt de container, oa. om de schaalbaarheid te vergroten. Daarnaast krijg je transactionaliteit gratis: elke methode-aanroep in je service is automatisch een geisoleerde transactie, en wordt dus ook automatisch teruggedraaid als er een exceptie optreedt. Dat zouden we dan allemaal zelf moeten programmeren, en ook nog eens in het transactie-mechanisme van de container moeten inprikken… als dat al kan. Ik denk verder dat je ook authorisatie kan laten regelen in zo’n bean, maar daar maken we (nog) geen gebruik van dus dat weet ik niet zeker.

    Geplaatst op 09 september 2010 om 9:10 Permalink

  • Jaap schreef:

    Jasper,

    Wellicht zou je toch eens moeten kijken naar een IoC container die DI voor je doet. Dat klinkt als iets wat je hier nodig hebt. Volgens mij kan Spring deze Beans gemakkelijk voor je beheren…

    Geplaatst op 09 september 2010 om 9:09 Permalink

  • Davld schreef:

    Beste Jasper,

    Kan je niet gewoon gebruik maken van generic methods? Ik weet niet precies hoe dat werkt in Java, maar in .NET kunnen we gewoon “getService().deleteOrder(orderId)” doen.

    David.

    Geplaatst op 09 september 2010 om 8:11 Permalink

    • Matthijs schreef:

      David,

      Dat zou inderdaad ook nog kunnen. Echter door direct een instantie aan te vragen op het type hoef je de enum niet meer te gebruiken.

      Geplaatst op 09 september 2010 om 8:14 Permalink

  • Matthijs schreef:

    Beste Jasper,

    Ik ben zelf een .Netter dus heb ik niet zo veel kaas gegeten van beans en zulks.
    Bij het plaatje wat je hierboven schetst moet ik direct denken aan een Singleton.
    Heb je dat overwogen of was deze niet toereikend?

    Matthijs

    Geplaatst op 09 september 2010 om 7:34 Permalink