C1F: Algorithmen in funktionale Teilstücke aufteilen

Ich kann Algorithmen in funktionale Teilstücke aufteilen.

Lernziele

# Lernziel Beantwortet in
1 Ich kann in einem gegebenen Algorithmus sinnvolle Teilschritte erkennen, die als eigenständige Funktionen implementiert werden können. 1. Teilfunktionen identifizieren
2 Ich kann einen Algorithmus so zerlegen, dass jede Teilfunktion genau eine Aufgabe erfüllt (Single Responsibility). 2. Funktionale Dekomposition
3 Ich kann beschreiben, wie die einzelnen Teilfunktionen zusammengesetzt werden, um den Gesamtalgorithmus zu bilden. 3. Zusammensetzung planen

1. Teilfunktionen identifizieren

Ein Algorithmus, der zu viel auf einmal macht, ist schwer zu testen und zu warten. Der erste Schritt ist, logische Teilschritte zu erkennen.

Beispiel: Monolithische Methode

List<String> process(List<String> data) {
    var result = new ArrayList<String>();
    for (var s : data) {
        var trimmed = s.strip();
        if (!trimmed.isEmpty()) {
            result.add(trimmed.toLowerCase());
        }
    }
    var unique = new ArrayList<>(new HashSet<>(result));
    Collections.sort(unique);
    return unique;
}

Dieser Code enthält vier logische Schritte:

Schritt Was passiert Mögliche Funktion
1 Whitespace entfernen und leere Strings filtern cleanEntries()
2 In Kleinbuchstaben umwandeln (Teil von cleanEntries())
3 Duplikate entfernen removeDuplicates()
4 Alphabetisch sortieren sortList()

2. Funktionale Dekomposition

Jede Teilfunktion bekommt genau eine Aufgabe (Single Responsibility), nimmt eine Liste entgegen und gibt eine neue Liste zurück:

// Teilfunktion 1: Bereinigen und leere Einträge entfernen
List<String> cleanEntries(List<String> data) {
    return data.stream()
        .map(String::strip)
        .filter(s -> !s.isEmpty())
        .map(String::toLowerCase)
        .toList();
}

// Teilfunktion 2: Duplikate entfernen
List<String> removeDuplicates(List<String> data) {
    return data.stream().distinct().toList();
}

// Teilfunktion 3: Sortieren
List<String> sortList(List<String> data) {
    return data.stream().sorted().toList();
}

Vorteile der Zerlegung

Vorteil Erklärung
Testbarkeit Jede Funktion kann isoliert mit eigenen Testfällen geprüft werden
Wiederverwendbarkeit removeDuplicates kann überall eingesetzt werden, nicht nur hier
Lesbarkeit Funktionsnamen dokumentieren die Logik
Wartbarkeit Änderungen betreffen nur die betroffene Teilfunktion

3. Zusammensetzung planen

Die Teilfunktionen werden so zusammengesetzt, dass der Output der einen zum Input der nächsten wird:

List<String> process(List<String> data) {
    var cleaned = cleanEntries(data);
    var unique = removeDuplicates(cleaned);
    return sortList(unique);
}

Weiteres Beispiel: Durchschnitt positiver Zahlen

Aufgabe: Berechne den Durchschnitt aller positiven Zahlen in einer Liste.

Zerlegung:

// Schritt 1: Nur positive Zahlen behalten
List<Integer> filterPositive(List<Integer> numbers) {
    return numbers.stream().filter(n -> n > 0).toList();
}

// Schritt 2: Summe berechnen
int sum(List<Integer> numbers) {
    return numbers.stream().reduce(0, Integer::sum);
}

// Schritt 3: Durchschnitt berechnen
double average(List<Integer> numbers) {
    return numbers.isEmpty() ? 0.0 : (double) sum(numbers) / numbers.size();
}

// Zusammensetzung
double averageOfPositive(List<Integer> numbers) {
    var positive = filterPositive(numbers);
    return average(positive);
}
var data = List.of(-3, 5, -1, 8, 2, -7, 4);
System.out.println(averageOfPositive(data));  // 4.75

Die Zerlegung folgt immer dem gleichen Muster: Teilschritte identifizieren, als eigenständige Funktionen implementieren, dann verketten. Jede Funktion verändert dabei die Eingabe nicht, sondern erzeugt einen neuen Wert.


This site uses Just the Docs, a documentation theme for Jekyll.