Das funktionale execute-around-pattern mit Java 8
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!