03.07.2019 von Simone Kämpf

First-Class-Funktionen gibt es in vielen funktionalen Sprachen und seit Version 8 auch in Java. Sie können in Variablen gespeichert, als Parameter an Methoden übergeben oder von diesen zurückgegeben werden.

Zugegebenermaßen war die Idee, Verhalten als Parameter durch den Code zu reichen, für mich am Anfang nicht gerade leicht zu verstehen. Eine Funktion als Objekt? Und wie sieht so eine Funktion eigentlich im Debugger aus? Aber nach den ersten Anlaufschwierigkeiten mochte ich das Konzept - für mich ist es mittlerweile die einfachste Möglichkeit, dem Computer mitzuteilen, was ich von ihm erwarte. Und bei fremdem Code ist es einfach nachzuvollziehen, was andere von ihm erwarten - immerhin sagte schon Donald Knuth: "Programming is the art of telling another human being what one wants the computer to do".

Aber wie genau sieht so eine Erste-Klasse-Funktion eigentlich aus? Nehmen wir als Beispiel einen Kunden des Onlineshops, der seine Rechnungsadresse ändern möchte. Irgendwo in den Tiefen des Codes habe ich eine Kundennummer und die neue Rechnungsadresse vorliegen. Die muss jetzt irgendwie an den Kunden, und der Kunde hat auch eine schöne Methode dafür: "aktualisiereRechungsadresse(RechnungsAdresse rechnungsAdresse)", der die Adresse übergeben wird. Eine Möglichkeit wäre das:

public class KundenService {
    KundenRepository repository;

    public Kunde aktualisiereRechnungsadresse(
    			Kundennummer kundennummer, 
                RechnungsAdresse rechnungsAdresse) {
                
        Kunde kunde = repository.ladeKunde(kundennummer);
        kunde.aktualisiereRechnungsadresse(rechnungsAdresse);
        repository.schreibeKunde(kunde);
        return kunde;
    }
}

public class KundenRepository {

    public Kunde ladeKunde(Kundennummer kundennummer) {
        return dbZugriff.ladeKunde(kundennummer);
    }

    public void schreibeKunde(Kunde kunde) {
        dbZugriff.schreibeKunde(kunde);
    }
}

Hier muss ich aber im KundenService (der in der Domäne, und nicht in der Datenbankschicht angesiedelt sein sollte) daran denken, den Kunden nach der Operation wieder in die Datenbank zu schreiben. Nichts, was ich in der Domäne machen möchte - dafür gibt es ja die Datenbankschicht. Eine andere Möglichkeit wäre:

public class KundenService {
    KundenRepository repository;

    public Kunde aktualisiereRechnungsadresse(
    			Kundennummer kundennummer, 
                RechnungsAdresse rechnungsAdresse) {
                
        return repository.aktualisiereRechnungsadresse(kundennummer, rechnungsAdresse);
    }
}

public class KundenRepository {

    public Kunde aktualisiereRechnungsadresse(
    			Kundennummer kundennummer, 
                RechnungsAdresse rechnungsAdresse) {
                
        Kunde kunde = dbZugriff.ladeKunde(kundennummer);
        kunde.aktualisiereRechnungsadresse(rechnungsAdresse);
        dbZugriff.schreibeKunde(kunde);
        return kunde;
    }
}

Das artet aber allzu schnell aus, wenn das KundenRepository für jeden Anwendungsfall eine eigene Methode bekommt. Außerdem besteht dann das umgekehrte Problem: die Datenbankschicht übernimmt Aufgaben der Domäne. Was ich eigentlich will:

  1. eine ID übergeben

  2. den Kunden suchen lassen

  3. den Kunden ändern lassen

  4. den Kunden speichern lassen

Wie die Datenbankschicht das macht, ist der Domäne dabei herzlich egal, so wie es der Datenbankschicht egal ist, welche Änderung die Domäne ausgeführt haben möchte.
Hier kommen Funktionen als Objekte erster Klasse ins Spiel. Das Konzept heißt so, weil Funktionen wie andere Objekte auch (im Beispiel etwa Kunde oder KundenRepository) durch den Code gereicht werden können. Vor Version 8 waren Funktionen in Java Objekte zweiter Klasse und durften nicht alleine verreisen.
Im Code sieht das so aus: in der Form objekt -> tuWas(objekt) wird eine Funktion übergeben, die objekt als Parameter übergeben bekommen und darauf tuWas aufrufen wird. Im folgenden Beispiel ist das ein Consumer, ein vordefinierter Datentyp für eine Funktion, die das Objekt aufnimmt und nichts zurückgibt. Es lassen sich aber alle möglichen Formen von Funktionen übergeben, solche mit einem oder mehreren Parametern, mit oder ohne Rückgabewert.

public class KundenService {
    KundenRepository repository;

    public Kunde aktualisiereRechnungsadresse(
    			Kundennummer kundennummer, 
                RechnungsAdresse rechnungsAdresse) {
                
        // die Funktion wird als anonyme Methode an aendereKunde übergeben        
        Kunde kunde = repository.aendereKunde(kundennummer, kunde ->
            kunde.aktualisiereRechnungsadresse(rechnungsAdresse)
        );
        return kunde;
    }
}

public class KundenRepository {

    public Kunde aendereKunde(
    			Kundennummer kundennummer, 
                Consumer<Kunde> consumer) {
                
        Kunde kunde = dbZugriff.ladeKunde(kundennummer);
        // die Funktion wird aufgerufen
        consumer.accept(kunde);
        dbZugriff.schreibeKunde(kunde);
        return kunde;
    }
}

Hier findet sich alles, was passieren soll, nah beieinander (und zwar im KundenService) und nicht über verschiedene Architekturschichten verteilt. Hole den Kunden und ändere seine Rechnungsadresse. Wie genau das mit dem Holen und Speichern geht, ist in dem Moment irrelevant. Programmieren ist Kommunikation, und für mich waren First-Class-Funktionen ein unglaubliches Aha-Erlebnis, wie ich noch klarer kommunizieren kann.

Die Autorin

Simone Kämpf
Seit 2017 Backendentwicklerin bei neuland.