a dev's blog

Some thoughts about thoughts.

Das funktionale execute-around-pattern mit Java 8

2016-02-17 Development Java

try… catch. catch.. catch…. catch. finally!

Na? Weiß jeder was ich meine? Ich gehe einfach mal von einer tief-seufzenden und bejahenden Antwort aus. ;)

Unser geliebtes Java bietet ein wirklich großartiges Exception-Handling. Das muss man ihm einfach zugestehen. Allerdings ist es leider auch so, dass wir als Java-Entwickler auch immer wieder den gleichen Code schreiben. Ein klassisches - allerdings wirklich das Paradebeispiel mit JBDC-Connections - ist folgendes:

public void executeSql(Connection conn, String sql) {
  PreparedStatement statement = null;
  try {
    statement = conn.prepareStatement(sql);
  } catch (SQLException e) {
    // Handle exception. I.e. some logging and you should return because a potential npe.
  }

  try {
    ResultSet results = statement.executeQuery();
    try {
      while (results.next()) {
        // Here is the important code for us. Just this one line - the rest is boilerplate.
        System.out.println(results.getString(0) + ", " + results.getString(1));
      }
    } finally {
      results.close();
    }
  } catch (SQLException e) {
    // Handle exception. (i.e. some logging)
  } finally {
    try {
      if (statement != null) {
        statement.close();
      }
    } catch (SQLException e) {
      // Handle exception. (i.e. some logging)
    }
  }
}

Wir gehen in dem Code zunächst davon aus, dass wir bereits eine Connection besitzen und wissen, was für ein SQL Statement ausgeführt werden soll. In den Zeilen 2-7 Erzeugen wir uns dann an formales PreparedStatement aus beiden Komponenten. Diesen Punkt könnte man eigentlich auch noch auslagern, da er ggf. Individuell sein kann. In Zeile 10 wird dann das Statement ausgeführt und anschließend in der while-Schleife über die Ergebnisse iteriert, wobei ich hier von einer mindestens 2-Spaltigen Ergebnismenge ausgehe, die Zeile für Zeile ausgegeben wird. Der ganze restliche Code ist ausschließlich Fehlerbehandlung und wird innerhalb einer Applikation meistens identisch sein.

Wenn wir jetzt eine Java-Anwendung ohne Hilfestellung irgendwelcher JDBC-Frameworks aufsetzen, müssen wir bei jedem Statement, was für ausführen wollen ungefär solchen Code schreiben, wie er oben steht; und das eigentlich nur für Zeile 14. Das ist nicht nur ärgerlich, sondern auch sehr unschön und sorgt in großem Maße für Unübersichtlichkeit.

Schöner ist die Welt mit Java8 und dem Execute-Around-Pattern

Aber Oracle sei Dank haben wir mit Java8 auch endlich die Möglichkeit bekommen funktionale Elemente zu nutzen. Heißt, wir können nicht nur simple Objekte von Methode zu Methode übergeben und damit arbeiten, sondern wir können jetzt auch Objekte übergeben, die eine veränderliche Funktionalität besitzten. Und allein mit dieser Erkenntnis können wir das Execute-Around-Pattern, welches unter Entwicklern, die schon länger mit funktionalen Sprachen arbeiten als alter Hut bezeichnet werden könnte auch endlich aufziehen. Im Kern geht es um folgendes:

Wir lagern genau den Teil des Codes, der immer gleich bleibt, in eine Hilfsmethode aus und übergeben dieser Hilfsmethode genau den Teil des Codes, der sich von Aufruf zu Aufruf unterscheidet.

Bevor ich in die Details gehe möchte ich noch kurz auf mein Github-Beispiel Projekt verweisen, das den gesamten Sourcecode zu dem Artikel beinhaltet.

Ok… Auf gehts. Wir legen uns zunächst eine Hilfsmethode mit dem statischen Teil des Codes an:

public static void executeSql(Connection conn, String sql, ResultSetConsumer action) {
  PreparedStatement statement = null;
  try {
    statement = conn.prepareStatement(sql);
  } catch (SQLException e) { /* ... */ }

  try {
    ResultSet results = statement.executeQuery();
    try {
      while (results.next()) {
        action.accept(results);
      }
    } finally {
      results.close();
    }
  } catch (SQLException e) { /* ... */ } finally {
    try {
      if (statement != null) {
        statement.close();
      }
    } catch (SQLException e) { /* ... */ }
  }
}

Was vielleicht bereits auffällt ist, dass im Methodenkopf der Parameter ResultSetConsumer zu finden ist. Dieser Consumer ist das Herzstück der Lösung, denn er ist ein ein so genannter SAM-Type (*S*ingle *A*bstract *M*ethod) und besitzt, wie der Name bereits mehr als verrät genau eine einzige abstrakte Methode.

@FunctionalInterface
public interface ResultSetConsumer {

  void accept(ResultSet rs) throws SQLException;
}

In diesem Fall heißt die Methode accept und erwartet als Parameter ein ResultSet und reicht auftretende SQLExceptions weiter an den Aufrufer der Funktion. Dieser letzte Teil ist wichtig, da Aufrufe wie ResultSet#getString diese Exceptions werfen können und wir wollen uns ja schließlich nicht bei jedem Aufruf um das Exception-Handling kümmern. Die Methode rufen wir im o.g. Beispiel in Zeile 11 auf. Das heißt an der Stelle wird die Funktionalität ausgeführt, die der Aufrufer dem Consumer einhaucht.

Was uns letztendlich in unserer Lösung noch fehlt ist ein Methodenaufruf der Hilfsmethode:

executeSql(conn, sql, new ResultSetConsumer() {
  @Override
  public void accept(ResultSet rs) throws SQLException {
    System.out.println(rs.getString(0) + ", " + rs.getString(1));
  }
});

Wir übergeben eine annomyme Implementierung des ResultSetCustomers an unsere Hilfsmethode. Wenn wir das ganze jetzt noch als Lambda-Ausdruck notieren, haben wir - in meinem Augen - eine sehr lesbare und ansehnliche Lösung geschaffen.

executeSql(conn, sql, rs -> {
  System.out.println(rs.getString(0) + ", " + rs.getString(1));
});

Fazit

Wir konnten sehen, wie wir wichtigen, aber immer wieder auftretenden Code reduzieren konnten. Dabei konnten wir ein hohes Maß an Lesbarkeit und Übersichtlichkeit gewinnen. Alles in allem finde ich dieses Pattern äußerst nützlich.

Cheers!