6 Moderne Coding-Prinzipien und Stil

Moderne Softwareentwicklung tendiert stark zu einem sauberen, modularen und wartbaren Code-Design. Dies reflektiert sich in diversen Prinzipien und Praktiken, die darauf abzielen, die Komplexität zu reduzieren und die Lesbarkeit sowie Testbarkeit von Code zu verbessern.

6.1 Einzelaufgabenfokus

Ein zentrales Konzept ist das Prinzip, dass “eine Sache eine Aufgabe haben soll”, oft auch als Single Responsibility Principle (SRP) bekannt. Dies bedeutet, dass jede Klasse, Methode oder Funktion genau eine Verantwortlichkeit haben sollte, was die Wartung und das Verständnis des Codes erleichtert. Durch die Aufteilung von Funktionalitäten in feingranulare Beans und Methoden im Spring Framework wird dieses Prinzip stark unterstützt und gefördert.

6.2 Imperativer vs. Deklarativer Stil

Traditioneller, imperativer Code, gekennzeichnet durch umfangreiche Fallunterscheidungen und Schleifenkonstruktionen, wird zunehmend durch einen deklarativen Stil ersetzt. Dieser fokussiert auf das „Was“ statt auf das „Wie“ einer Operation. Anstelle von expliziten Schleifen und Verzweigungen nutzt moderner Code Konstrukte wie Streams und Lambdas in Java, um Operationen wie Filtern, Transformieren oder Aggregieren von Daten in einer fließenden, leicht verständlichen Art und Weise zu beschreiben.

6.3 Chain-of-Responsibility und Streaming

Fallunterscheidungen und komplexe Verzweigungen sind in modernem Code oft durch das Chain-of-Responsibility-Muster oder durch Streaming-APIs ersetzt. Diese Ansätze ermöglichen es, Operationen in einer linearen, übersichtlichen Form zu schreiben, die sowohl die Lesbarkeit als auch die Wartbarkeit des Codes verbessert. Streams, zusammen mit Lambda-Ausdrücken, bieten eine mächtige, flexible Möglichkeit, mit Datenkollektionen zu arbeiten, indem sie es erlauben, Operationen wie Filterung, Sortierung oder Mapping in einer deklarativen Weise zu definieren.

6.4 Vorteile des modernen Ansatzes

6.5 Unterschied zu einsteigerhaftem oder chaotischem Code

Einsteiger oder weniger disziplinierte Entwickler neigen oft dazu, Code zu schreiben, der stark imperativ ist, viele Zustände verwaltet und komplexe Verzweigungen aufweist. Solcher Code kann schnell unübersichtlich, schwer zu verstehen und zu warten werden. Im Gegensatz dazu basiert moderner, professioneller Code auf Prinzipien der Klarheit, Modularität und Wiederverwendbarkeit, wodurch er auch in komplexen Anwendungen effizient und effektiv bleibt.

6.6 Beispiel

6.6.1 Imperativer Ansatz

Zunächst ein Beispiel im imperativen Stil. Angenommen, wir haben eine Liste von Personen, und wir möchten alle Personen herausfiltern, die älter als 18 Jahre sind, und diese dann nach dem Namen sortieren.

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;

class Person {
    String name;
    int age;
    
    Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
    
    public String getName() {
        return name;
    }
    
    public int getAge() {
        return age;
    }
}

public class ImperativeExample {
    public static void main(String[] args) {
        List<Person> people = new ArrayList<>();
        people.add(new Person("Alice", 22));
        people.add(new Person("Bob", 17));
        people.add(new Person("Charlie", 24));
        
        List<Person> adults = new ArrayList<>();
        for (Person person : people) {
            if (person.getAge() > 18) {
                adults.add(person);
            }
        }
        
        Collections.sort(adults, new Comparator<Person>() {
            public int compare(Person p1, Person p2) {
                return p1.getName().compareTo(p2.getName());
            }
        });
        
        for (Person adult : adults) {
            System.out.println(adult.getName());
        }
    }
}

6.6.2 Deklarativer Ansatz mit Streams

Jetzt lösen wir die gleiche Aufgabe mit der Stream-API in Java, was den Code wesentlich kompakter und lesbarer macht. Das Chain-of-Command-Pattern ermöglicht es uns, Operationen in einer fließenden Art und Weise zu verketten.

import java.util.List;
import java.util.stream.Collectors;

public class StreamExample {
    public static void main(String[] args) {
        List<Person> people = List.of(
            new Person("Alice", 22),
            new Person("Bob", 17),
            new Person("Charlie", 24)
        );
        
        List<Person> sortedAdults = people.stream()
            .filter(person -> person.getAge() > 18)
            .sorted(Comparator.comparing(Person::getName))
            .collect(Collectors.toList());
        
        sortedAdults.forEach(adult -> System.out.println(adult.getName()));
    }
}

In diesem Beispiel verwendet stream() die Collection als Quelle für die Operationen, die folgen. filter() verwendet einen Lambda-Ausdruck, um nur die Personen herauszufiltern, die älter als 18 sind. Anschließend werden diese mit sorted() nach dem Namen sortiert. Das Endresultat wird mit collect(Collectors.toList()) in eine neue Liste gesammelt.

Dies demonstriert, wie Streams und Lambdas den Code nicht nur kürzer und klarer machen, sondern auch die Lesbarkeit und Wartbarkeit verbessern, indem sie ein hohes Maß an Abstraktion und Deklarativität bieten.