Een methode met variabel return type

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.