Kolumne: Enterprise Tales

Funktionale Programmierung in Java: Was wäre, wenn wir alles ganz anders machen würden?

Sven Kölpin

Der Kern von objektorientierter Programmierung ist die Kapselung von Daten und den darauf operierenden Methoden. Das macht Objektorientierung zu einem guten Kandidaten, um Businesslogik zu implementieren, da diese ja in der Regel eine Verknüpfung von Daten und Operationen darstellt. Richtig angewendet, lassen sich sehr gut lesbare und wartungsfreundliche Businessanwendungen mit objektorientierter Programmierung schreiben.

Objektorientierung hat allerdings einen großen Nachteil. Sie ist von Grund auf statusbehaftet. Das wird zum Problem, wenn es um parallele Zugriffe geht. Parallele Zugriffe auf dasselbe Objekt lassen sich mit objektorientierter Programmierung nicht performant und gleichzeitig wartbar realisieren. Beim Versuch das zu tun entsteht eine Mischung aus Infrastrukturcode und Businesslogik. Eine Vermischung beider Aspekte erscheint natürlich nicht sinnvoll. Hinzu kommt, dass objektorientierte Programmierung generell nicht gut darin ist, Infrastrukturcode zu schreiben. Leider bestehen unsere Anwendungen nur zu einem Teil aus Businesslogik. Ein nicht unerheblicher Teil ist Infrastrukturcode.

In den vergangenen Jahren ist zu beobachten gewesen, dass sich die Enterprise-Java-Frameworks daranmachen, die Trennung von Infrastrukturcode und Businesslogik voranzutreiben. Vorreiter ist dabei nach wie vor das Spring-Framework, aber auch der Enterprise-Java-Standard zieht regelmäßig nach. Die Trennung hat das durchaus zu begrüßende Ziel, dass sich der allgemeine Entwickler auf die Implementierung der Businesslogik fokussieren kann, während das Framework die Infrastrukturlogik quasi unbemerkt im Hintergrund erledigt.

Das Ergebnis ist allerdings eine Flut von Annotations, hinter denen sich der Infrastrukturcode häufig so gut versteckt, dass dem Entwickler nicht klar ist, was dahinter tatsächlich passiert. Das wird natürlich dann zum Problem, wenn der Infrastrukturcode nicht so funktioniert wie vom Entwickler erwartet. Zudem kann es passieren, dass vor lauter Annotations die Businesslogik im Code nicht mehr wiederzufinden ist. Einige Leute sprechen angesichts der Annotation-Flut schon von der „Annotation Hell“, in Anlehnung an die „XML Hell“, die die Älteren unter uns noch aus J2EE-Zeiten kennen.

Auf die Spitze getrieben hat diesen Tatbestand Greg Young bereits in seiner Keynote zur QCon 2013 mit seinem Talk „8 Lines of Code“ [1]. Auch wenn sich die Keynote auf C#-Code bezog, lässt sie sich sehr gut auf Enterprise-Java-Code übertragen. Als Aufhänger nimmt Young in seinem Vortrag einen Domänenservice, der übersetzt in die Enterprise-Java-Welt ungefähr so aussieht wie in Listing 1.

@Transactional
@RolesAllowed("admin")
@Logged
@EatsException
@DoesBadThingsWhenYouArentWatching
public class DeactivateInventoryItemService {

  private ItemRepository repository;
  
  @Inject
  public DeactivateInventoryItem(ItemRepository repository) {
    this.repository = repository;
  }

  public void deactivate(UUID id, String reason) {
    InventoryItem item = repository.getById(id);
    item.deactivate(reason);
  }
}

Magie leichter verständlich

Greg Young stellt die These auf, dass sich derselbe Code ohne Annotations funktional deutlich wartbarer schreiben ließe. So würde z. B. das Konzept der Dependency Injection, das es in funktionaler Programmierung nicht gibt, durch einfache Methodenparameter ersetzt, die dann durch Currying [2] an zentraler Stelle konfiguriert werden könnten:

BiConsumer<ItemRepository, DeactivateCommand> deactivateMethod
  = DeactivateInventoryItemService::deactivate;
ItemRepository repository = ...
Consumer deactivateExecutor
  = command -> deactivateMethod.accept(repository, command);
Consumer transactionalDeactivateExecutor
  = command -> transactional(deactivateExecutor);

Die beiden Parameter vom Typ UUID und String werden dort zunächst zu einem DeactivateCommand zusammengefasst, sodass im Service eine statische Methode mit der Signatur void deactivate(ItemRepository repository, DeactivateCommand command) entsteht. Transaktionalität könnte durch eine einfache Wrapper-Funktion realisiert und Scopes (wie CDI und Spring sie bieten) durch Provider ersetzt werden, die bei Bedarf die benötigte Instanz liefern. In einer solchen Anwendung könnten wir komplett auf CDI-Annotations verzichten. Es findet keine Magie statt, alle Aufrufe sind reine Java-Methodenaufrufe.

Provider repositoryProvider = ...
Consumer deactivateExecutor
  = command -> deactivateMethod.accept(repositoryProvider.get(), command);

Macht die funktionale Programmierung in Java 8 unsere Enterprise-Java-Frameworks also komplett überflüssig? Glaubt man Young, ist das der Fall. Jedenfalls plädiert er dafür, die Frameworks nicht mehr zu verwenden, um unseren Code wieder besser zu verstehen. In der Konsequenz bedeutet das, dass wir unseren Infrastrukturcode wieder selbst schreiben müssen. Mit diesem Fazit stimme ich allerdings nicht überein. Auch in einer funktionalen Welt benötigen wir Transaktionen und Frameworks, mit denen wir z. B. auf die Datenbank zugreifen. Es ist nicht gerade einfach, z. B. eine korrekte Transaktionsbehandlungsroutine schreiben, die auch in allen Rand- und Fehlerfällen funktioniert. Natürlich ist es möglich, diesen Code selbst zu schreiben, aber wollen wir das auch? Noch dazu benötigt der Entwickler denselben Code in jedem Projekt erneut. Das Ergebnis wäre, dass dieser Infrastrukturcode von Projekt zu Projekt kopiert werden oder gleich in eine Bibliothek ausgelagert würde. Damit hätten wir dann wieder ein Framework geschaffen. Das Ergebnis wäre also, dass wir irgendwann funktionale Frameworks hätten, bei denen auch nicht jeder Entwickler versteht, was unter der Haube passiert. Somit hätte man also nicht viel gewonnen.

Funktionale Programmierung als nächste Evolutionsstufe

Aber auch wenn man nicht so weit gehen möchte, wie Greg Young in seiner Keynote vorschlägt, ist der Gedanke interessant, durch funktionale Programmierung einen Ausweg aus der „Annotation Hell“ zu finden. Der Talk „Java EE: Extendable and Functional“, den David Blevins, Mitbegründer von OpenEJB, Apache Geronimo und Apache TomEE und außerdem JCP-Mitglied, auf der JavaZone 2016 [3] gehalten hat, geht in dieselbe Richtung. Blevins stellt darin die These auf, dass funktionale Programmierung der nächste Schritt in Enterprise Java sein könnte, wenn es darum geht, die Anwendung zu konfigurieren. Er stellt funktionale Programmierung auf eine Ebene mit der bereits erwähnten XML-basierten Konfiguration, die, wie er sagt, 1998 eingeführt wurde und 2001 ihren Höhepunkt hatte, und der Annotation-basierten Konfiguration, die 2006 eingeführt wurde und 2009 ihren Höhepunkt hatte. Funktionale Programmierung wurde in Java 2014 mit Java 8 eingeführt, und es bleibt abzuwarten, wann sie ihren Höhepunkt erreicht haben wird.

In Blevins Talk kommen einige Beispiele vor, wie in Enterprise Java bereits jetzt oder (durch kleine API-Erweiterungen) in der Zukunft viel Code gespart werden könnte. Ein gekürztes Beispiel sehen wir in den Listings 4 und 5 im Vergleich. Für statische Timer gibt es in Java EE die @Schedule Annotation. Wenn ich meinen Timer aber zur Laufzeit konfigurieren will, bin ich auf den TimerService angewiesen. Möchte ich mit diesem nun innerhalb einer Klasse unterschiedliche Aktionen auslösen, habe ich das Problem, dass es pro Klasse nur eine Methode geben darf, die mit @Timeout annotiert ist. Ich muss also innerhalb dieser Methode unterscheiden, was zu tun ist (Listing 2) [4]. Solcher Code ist hässlich und fehleranfällig. Mit funktionaler Programmierung lässt sich das Problem eleganter lösen, indem ich einfach die auszuführende Methode direkt mitgebe (Listing 3) [4]. Der gezeigte Code funktioniert bereits jetzt in Java EE 7, solange man lokal arbeitet und nicht versucht, den Timer tatsächlich zu passivieren und TimerInfo zu serialisieren. Wünschenswert für die Zukunft (also z. B. für die nächste Enterprise-Java-Version) wäre natürlich, dass ganz auf die @Timeout-Methode verzichtet werden könnte. Der Container könnte erkennen, dass ihm eine Method Reference übergeben wurde und bei einem Timeout genau diese Methode aufrufen, anstatt eine mit @Timer annotierte Methode zu suchen.

public class FarmerBrown {
  ...
  @PostConstruct
  public void construct() {
    {
      TimerConfig config = new TimerConfig("plantTheCorn", false);
      timerService.createCalendarTimer(expression("0 0 8 20-Last 5"), config);
      timerService.createCalendarTimer(expression("0 0 8 1-10 6"), config);
    }
    {
      TimerConfig config = new TimerConfig("harvestTheCorn", false);
      timerService.createCalendarTimer(expression("0 0 8 20-Last 9"), config);
      timerService.createCalendarTimer(expression("0 0 8 1-10 10"), config);
    }
  }

  @Timeout
  public void timeout(Timer timer) {
    if ("plantTheCorn".equals(timer.getInfo())) {
      plantTheCorn();
    } else if ("harvestTheCorn".equals(timer.getInfo())) {
      harvestTheCorn();
    }
  }
}
public class FarmerBrown {
  ...	
  @PostConstruct
  public void construct() {
    {
      Serializable plantTheCorn
        = (Serializable & Runnable) this::plantTheCorn;
      TimerConfig config = new TimerConfig(plantTheCorn, false);
      timerService.createCalendarTimer(expression("0 0 8 20-Last 5"), config);
      timerService.createCalendarTimer(expression("0 0 8 1-10 6"), config);
    }
    {
      Serializable harvestTheCorn
        = (Serializable & Runnable) this::harvestTheCorn;
      TimerConfig config = new TimerConfig(harvestTheCorn, false);
      timerService.createCalendarTimer(expression("0 0 8 20-Last 9"), config);
      timerService.createCalendarTimer(expression("0 0 8 1-10 10"), config);
    }
  }

  @Timeout
  public void timeout(Timer timer) {
    ((Runnable) timer.getInfo()).run();

}

Businesslogik mit funktionaler Programmierung umsetzen

Bisher haben wir uns nur die Umsetzung des Infrastrukturcodes mittels funktionaler Programmierung angesehen. Verfechter von funktionaler Programmierung sehen aber gerade in der Realisierung der Businesslogik ihre große Stärke. Die liegt demnach darin, dass reine funktionale Programme nur mit unveränderlichen (immutable) Datenstrukturen arbeiten und dadurch leichter testbar, wartbar und obendrein performanter sind.

Wir arbeiten seit einigen Jahren nach Domain-driven Design. Hier existiert unter anderem das Konzept der Value Objects, bei denen es sich um genau solche unveränderlichen Datenstrukturen handelt [5]. Aus der Erfahrung kann ich sagen, dass sich mit Value Objects tatsächlich besser wartbarer Code schreiben lässt. Ganz gut dazu passt der Blogbeitrag von John A. De Goes [6], in dem er herausstellt, dass die Konzepte des Functional Programming, insbesondere Higher Order Functions, Immutable Data Structures, Strong Types und Pure Functions (Funktionen ohne Seiteneffekt) dazu geeignet sind, gute Software zu schreiben. Allerdings stellt er auch fest, dass sie nicht zwangsläufig dazu führen, dass funktionale Software gute Software wird. Ich möchte an dieser Stelle ergänzen, dass man diese Konstrukte teilweise durchaus auch in objektorientierten Programmiersprachen einsetzen kann und so auch ohne funktionale Programmierung gut wartbare Software entsteht. Einziger Vorteil ist, dass in rein funktionalen Programmiersprachen wie z. B. Clojure das Erstellen von immutable Datenstrukturen mit deutlich weniger Zeilen Code möglich ist als in Java.

Fazit

Die erwähnten Konzepte von funktionaler Programmierung (Higher Order Functions, Immutable Data Structures, Strong Types und Pure Functions) sind Patterns, die geeignet sind, um damit sehr wartungsfreundliche Software zu schreiben. Unabhängig davon, ob man ansonsten komplett funktional programmiert, ist der Einsatz dieser Konzepte in Enterprise Programming eine gute Idee. Darüber hinaus haben wir gesehen, wie es in Zukunft möglich sein könnte, über Funktionsreferenzen einen Teil der in Enterprise Java benötigten Annotations loszuwerden. Ein Großteil der Infrastrukturkonfiguration einer Enterprise-Java-Anwendung könnte dann stattdessen mit Funktionen erfolgen.

Wer allerdings erwägt, komplett auf funktionale Programmierung umzusteigen, sollte sich fragen, ob Java dann tatsächlich die richtige Programmiersprache für ihn ist. Wahrscheinlich ist es in diesem Fall sinnvoll, zu einer funktionalen Sprache zu wechseln, die die entsprechenden Konzepte nativ unterstützt, wie Haskell, Clojure oder Kotlin. Das Featureset in Bezug auf funktionale Programmierung ist bei diesen Sprachen zum Teil deutlich höher als in Java. Außerdem findet man dort schon jetzt häufig eine bessere IDE-Unterstützung, als es sie für die funktionalen Konstrukte in Java gibt. Wer Funktionen allerdings nur als Ausweg aus der „Annotation Hell“ sieht, der kann eventuell darauf hoffen, dass die nächste Enterprise-Java-Version tatsächlich Konfigurations-APIs auf Basis von Funktionen zur Verfügung stellt und damit Annotations teilweise oder komplett überflüssig macht.

Geschrieben von
Sven Kölpin
Sven Kölpin
Sven Kölpin ist Enterprise Developer bei der open knowledge GmbH in Oldenburg. Sein Schwerpunkt liegt auf der Entwicklung webbasierter Enterprise-Lösungen mittels Java EE.
Kommentare

Hinterlasse einen Kommentar

4 Kommentare auf "Funktionale Programmierung in Java: Was wäre, wenn wir alles ganz anders machen würden?"

avatar
400
  Subscribe  
Benachrichtige mich zu:
Jost Schwider
Gast

Schon seit 2004 gibt es mit Groovy eine „eierlegende Wollmichsau“, welche funktionale und objektorientierte Aspekte in einer deutlich weniger geschwätzigen Java-Variante bietet.
=> Best of both worlds, da muss man also noch nicht mal auf neue Sprachen wie Kotlin verweisen.

Arno Nyhm
Gast

Referenzen [5] und [6] fehlen in der Linkliste am Ende

Redaktion JAXenter
Mitglied

Die fehlenden Referenzen wurden ergänzt.

Vielen Dank für den Hinweis!

Torsten
Gast

Wie wäre es mit Scala, Java kopiert ja schon eine Menge an Funktionalitäten.