Datenorientierte Programmierung mit Java

Java und Objektorientierte Programmierung

Java gibt es nun schon seit mehr als 30 Jahren und wurde von Grund auf als rein objektorientierte Sprache konzipiert. Das Mantra „alles ist ein Objekt“ (nun ja, fast – Primitive ausgenommen) ist vom ersten Tag an tief in der Java DNA verankert. Diese objektorientierte Grundlage hat dabei geholfen, Java zu einer der beliebtesten Programmiersprachen der Welt zu machen. Javaanwendungen gibt es in einem großen Spektrum, angefangen bei Microservices, über Unternehmensanwendungen bis hin zu Android-Mobile-Apps.

Das OO Paradigma hat gute Dienste geleistet und es uns ermöglicht, komplexe und wartbare Systeme zu erstellen. Die Sprache hat sich in den letzten Jahren aber weiterentwickelt und ist gereift. Diese Entwicklungen haben auch die Erkenntnis gebracht, dass OO nicht immer der beste Ansatz für jeden Zweck ist.

Die Objektorientierte Programmierung basiert auf fundamentalen Prinzipien: Kapselung, Vererbung, Polymorphismus und Abstraktion. Diese Prinzipien ermöglichen es uns, komplexe Systeme zu modellieren. Dabei bündeln wir Daten mit Methoden, die auf diesen Daten operieren, in Klassen.

Objektorientierte Programmierung bietet erhebliche Vorteile: Wiederverwendbarkeit von Code durch Vererbung, intuitive Modellierung realer Entitäten und klare Trennung der Zuständigkeiten durch Kapselung. Allerdings gibt es auch Nachteile: Tiefe Vererbungshierarchien können fragil und schwer wartbar werden. Die Veränderlichkeit der Daten kann zu unerwarteten Seiteneffekten führen. Eine enge Kopplung zwischen Daten und Verhalten kann Code schwerer nachvollziehbar und schwer testbar machen.

Gibt es Möglichkeiten in Java, diese Nachteile in bestimmten Fällen zu vermeiden?
Um dieser Frage nachzugehen, schauen wir uns neue Features an, die dem JDK in den letzten Jahren hinzugefügt wurden. Wenn wir diese Features richtig kombinieren, können wir möglicherweise einige dieser traditionellen OO-Nachteile abmildern.

Algebraische Datentypen (ADT)

Ohje… da geht’s schon los. Algebra!? Ernsthaft?!

Keine Angst. Es ist einfacher, als es auf den ersten Blick scheint, zumindest wenn wir uns nur die Sachen anschauen, die hier relevant sind.

In computer programming an algebraic data type is a composite data type — a type formed by combining other types.
An algebraic data type is defined by two key constructions: a sum and a product.
These are sometimes referred to as “Or” and “And” types.


Wikipedia

OK — das klingt eher nach unserem täglichen Umgang mit Java und Klassen.

Schauen wir uns also mal näher an, was “Sum Types” (Summentypen) und “Product Types” (Produkttypen) sind und wie man diese in Java abbildet.

Summentypen — “OR”

Der Summentyp ist eine Auswahl zwischen verschiedenen Möglichkeiten. Er repräsentiert einen Wert aus mehreren definierten Varianten. Zum Beispiel: Eine geometrische Form kann entweder ein Kreis, ein Rechteck oder ein Dreieck sein. Es gäbe natürlich noch andere geometrische Formen – diese müssen dann aber als Wahlmöglichkeit definiert sein.

Ein Beispiel für einen Summentyp in Java ist Enum:

enum Shape {
    RECTANGLE, CIRCLE, TRIANGLE;
}

Ein anderes, ausdrucksstärkeres Beispiel ist Sealed Types.

sealed Types

Sealed Types gibt es seit JDK 17.
Es gibt sealed Klassen und sealed Interfaces.

A sealed class or Interface can be extended or implemented only by those classes and interfaces permitted to do so.

JEP 409: Sealed Classes
sealed interface Shape permits Circle, Rectangle, Triangle {}

Im obigen Code dürfen nur die Typen Circle, Rectangle, and Triangle das Interface Shape implementieren. Sollten wir noch andere Typen wie beispielsweise Hexagon oder Oval haben, ist es für diese Typen verboten, dass Shape Interface zu implementieren.

Auf diese Weise haben wir eine kontrollierte und endliche Menge von Typen, die zur Kompilierzeit bekannt sind. Das ist eine wertvolle Information, die der Compiler nutzen kann.

Produkttypen — “AND”

Ein Produkttyp enthält mehrere Komponententypen. Der Wert eines Produkttyps enthält einen Wert für jeden enthaltenen Komponententyp.

Entsprechend dieser Definition ist jede Klasse, die nur Datenfelder hat und keine Methoden anbietet, um den Inhalt dieser Datenfelder zu manipulieren, ein Produkttyp. Man nennt so etwas auch eine immutable Klasse.

Seit JDK 16 gibt es dafür sogar einen eigenen Java-Typ: Record.

Records

Seit JDK 16 gibt es Records in Java. Records sind sozusagen Tupel mit einem Namen.

carriers of immutable data and can be thought of as nominal tuples

JEP 395: Records
record Rectangle(int a, int b) {}
record Circle(int radius) {}
record Triangle(int a, int b, int angle) {}

Einmal initialisiert können die in einem Record enthaltenen Daten nicht mehr verändert werden (auch nicht per Reflexion!). Diese Eigenschaft prädestiniert Records zum Modellieren von Daten – nicht mehr und nicht weniger.

Falls man die Daten eines Records ändern will, geht das nur, indem man ein neues Record erstellt, welches dann die geänderten Daten enthält. JEP 468: Derived Record Creation (auch bekannt als “Withers”) wird die Duplizierung von Records (mit leicht geänderten Daten) wesentlich erleichtern.

Die Daten, die in einem Record enthalten sind, sollten vor der Instantiierung validiert werden. Das kann spätestens im Konstruktor des Records erfolgen. Auf diese Art wird die Erzeugung von invaliden Records vermieden.

Weshalb sind Algebraische Datentypen für uns interessant?

Die Antwort lautet: Pattern Matching.

Pattern Matching

Beim Pattern Matching wird geprüft, ob die Struktur und der Inhalt eines Wertes einem bestimmten Muster entspricht. Is das der Fall, werden dessen Bestandteile destrukturiert (gebunden/extrahiert), um sie weiterzuverwenden.

Zu den wichtigsten Verhaltensweisen des Pattern Matchings gehören:

  • Testen: Überprüfung der genauen Strukturübereinstimmung (z.B. ist eine Klasse eine Instanz von Shape?)
  • Binden: bindet eine Struktur (oder Teile davon) an einen Namen (z.B., bindet Circle c an die Variable c, und ermöglicht den Zugriff auf c.radius)
  • Vollständigkeit: Stellt sicher, dass alle Fälle abgedeckt sind (wird vom Compiler für ADTs überprüft)
  • Wächter (Guards): Hinzufügen von Bedingungen (z.B., Circle c when c.radius > 0)

Pattern Matching for instanceof ist seit JDK 16 Bestandteil von Java. Statt wie bisher üblich noch ein extra Casting nach einer instanceof Üperprufung zum machen

if (o instanceof Shape) {
    Shape s = (Shape) o;
    ...
}

kann man jetzt direkt eine Variable definieren, die schon den richtigen Typ hat. Das verkürzt nicht nur die Prüfung, sondern ist auch weniger fehleranfällig.

if (o instanceof Shape s) {
    ...
}

Wenn wir da nun noch mit Pattern Matching for switch kombinieren, welches seit JDK 21 final ist, können wir die unterschiedlichen Shape Implementierungen in einem switch Statement behandeln. Da Shape ein sealed Interface ist, ist zur Kompilierzeit schon bekannt, welche Klassen das Interface implementieren dürfen. Der Compiler kann daher prüfen, ob im switch Statement alle Möglichkeiten abgedeckt sind. Das macht eine default Klausel überflüssig, was auch bevorzugt ist.

double area(Object o) {
    if (o instanceof Shape s) {
        return switch (s) {
            case Rectangle r -> r.a() * r.b();
            case Circle c -> Math.PI * Math.pow(c.radius(), 2);
            case Triangle t -> 0.5 * t.a() * t.b() * Math.sin(t.angle());
            // no default clause needed    
        };
    }
    throw new IllegalArgumentException("Object is not a known shape.");
}

Wenn wir jetzt zusätzlich noch JEP 440: Record Patterns verwenden, was ebenfalls seit JDK 21 final ist, so kann das switch Statement noch prägnanter geschrieben werden:

double area(Object o) {
        if (o instanceof Shape s) {
            return switch (s) {
                case Rectangle(int a, int b) -> a * b;
                case Circle(int radius) -> Math.PI * Math.pow(radius, 2);
                case Triangle(int a, int b, int angle) -> 0.5 * a * b * Math.sin(angle);
            };
        }
        throw new IllegalArgumentException("Object is not a known shape.");
    }

Hier werden die Elemente der Records an Variablen gebunden, auf die dann zur Berechnung der Ergebnisse zugegriffen werden kann.

Wenn wir jetzt eine neue geometrische Form (beispielsweise Hexagon) zu den erlaubten Formen hinzufügen, wird uns der Compiler warnen, dass unser switch Statement nicht alle möglichen Werte abdeckt, also unvollständig ist. Das ist allerdings nur möglich, wenn kein default Branch vorhanden ist.

Eine weitere wichtige Beobachtung ist die Trennung der Definition der Daten von den Operationen auf den Daten. Die area Methode im obigen Beispiel ist nicht Teil des sealed Interfaces, sondern kann sich an anderer Stelle befinden.

Mit JEP 456: Unnamed Variables & Patterns in JDK 22 und Primitive Types in Patterns, instanceof, and switch (aktuell in Preview) gibt es interessante Erweiterungen zum Thema Pattern Matching.

Was ist Datenorientierte Programmierung (DOP)?

Was soll also diese ganze Diskussion über algebraische Datentypen, Pattern Matching, Records, versiegelte Klassen und so weiter?
Vielleicht können wir diese Features so kombinieren, dass ihr Zusammenspiel mehr ergibt als die Summe ihrer einzelnen Teile. Tatsächlich haben wir mit sealed Interfaces, Records, Pattern Matching für Switch und Record Patterns alle Zutaten für Datenorientierte Programmierung. Datenorientierte Programmierung sollte spezifischen Prinzipien folgen, die unseren Fokus von Objekten hin zu Daten verlagern.

4 Prizipien Datenorientierter Programmierung

Gemäß Brian Goetz’ wegweisender Arbeit zu diesem Thema folgt Datenorientierte Programmierung vier Schlüsselprinzipien [1]:

  1. Modelliere die Daten, die gesamten Daten und nichts als die Daten
  2. Daten sind unveränderlich
  3. Validiere die Daten an den Grenzen
  4. Verhindere illegale Zustände

Schauen wir uns an, wie die schon erwähnten Java Features jedes dieser Prinzipien unterstützen.

Prinzip 1: Modelliere die Daten, die gesamten Daten und nichts als die Daten

Dieses Prinzip betont die Trennung von Datenmodellierung und Verhalten. Statt Daten und Methoden in Klassen zu bündeln, modellieren wir Datenstrukturen als reine Datenträger.

Dies kann durch eine Kombination aus sealed Interfaces und Records erreicht werden. Sealed Interfaces definieren die Menge der möglichen Varianten, während Records die Struktur jeder Variante definieren. Diese Trennung ermöglicht es uns, neue Operationen hinzuzufügen, ohne die Datendefinitionen selbst zu verändern.

sealed interface Shape permits Circle, Rectangle, Triangle {}
record Circle(int radius) implements Shape {}
record Rectangle(int a, int b) implements Shape {}
record Triangle(int a, int b, int angle) implements Shape {}

// Wenn es nur wenige kleine erlaubte Unterklassen gibt, kann es praktisch sein, sie in der Datei des sealed Interfaces zu deklarieren.
// Wenn sie auf diese Weise deklariert werden, kann man die permits-Klausel weglassen, und der Compiler leitet die erlaubten Unterklassen aus den Deklarationen ab.
sealed interface Shape {
    record Circle(int radius) implements Shape {}
    record Rectangle(int a, int b) implements Shape {}
    record Triangle(int a, int b, int angle) implements Shape {}
}

Prinzip 2: Daten sind unveränderlich

Records sind per Definition unveränderlich. Einmal erstellt, können ihre Komponentenwerte nicht mehr geändert werden. Dies eliminiert ganze Kategorien von Fehlern, die durch unerwartete Zustandsmutationen entstehen. Der Code ist leichter nachvollziehbar – insbesondere in nebenläufigen Kontexten.

Die Unveränderlichkeit von Records stellt sicher, dass Daten vorhersehbar durch unsere Programme fließen, ohne versteckte Seiteneffekte.

Prinzip 3: Validiere die Daten an den Grenzen

Daten sollten validiert werden, wenn sie in das System eintreten (an der Grenze), nicht überall im Code verteilt. Der späteste Moment zur Validierung ist innerhalb des Record-Konstruktors. Mit der Validierung stellen wir sicher, dass es keine invaliden Datenzustände in unserem System geben kann.

record Circle(int radius) implements Shape {
    public Circle {
        if (radius <= 0) {
            throw new IllegalArgumentException("Radius must be positive");
        }
    }
}

Prinzip 4: Verhindere illegale Zustände

Dieses Prinzip ermutigt uns, unsere Datentypen so zu gestalten, dass ungültige Zustände schlichtweg nicht im Code darstellbar sind. Durch Validierung in Record-Konstruktoren verhindern wir die Erstellung ungültiger Daten.

Darüber hinaus stellt Pattern Matching mit Switch auf Sealed Types die Vollständigkeit sicher – der Compiler erlaubt es uns nicht, Fälle zu übersehen. Wenn wir auf einen Default-Branch verzichten, warnt uns der Compiler, falls wir einem sealed Interface eine neue Variante hinzufügen, ohne sie in bestehenden Switch-Anweisungen zu behandeln. Wir müssen also nicht alle betroffenen Stellen im Code selbst suchen.

// Compiler ensures all Shape variants are handled
String stringify (Shape s) {
    return switch (s) {
        case Circle c -> "It's a circle";
        case Rectangle r -> "It's a rectangle";
        case Triangle t -> "it's a triangle";
        // Kein default-Branch notwendig - der Compiler "weiß", dass die case-Branches vollständig sind.
    };
}

Wann ist Datenorientierte Programmierung nützlich und sinnvoll?

Datenorientierte Programmierung glänzt in mehreren Szenarien:

Domänenmodellierung: Bei der Modellierung komplexer Domänen mit klar definierten Datenstrukturen und Operationen bietet DOP Klarheit und Typsicherheit. Finanzsysteme, medizinische Datensätze und Konfigurationsdaten sind hervorragende Kandidaten.

Datentransformations-Pipelines: Systeme, die Daten primär von einer Form in eine andere transformieren, profitieren stark von unveränderlichen Datenstrukturen und Pattern Matching. ETL-Prozesse, Compiler und Datenverarbeitungs-Frameworks sind hierfür prädestiniert.

APIs und Datentransfer: Beim Entwurf von APIs oder bei der Arbeit mit externen Datenquellen bieten unveränderliche Records klare Verträge und verhindern versehentliche Mutationen.

Nebenläufige Systeme: Die Garantie der Unveränderlichkeit von DOP erleichtert das Schreiben von korrektem nebenläufigem Code ohne komplexe Synchronisierung.

Algebraische Problemdomänen: Jede Domäne, die sich natürlich auf algebraische Datentypen abbilden lässt – wie Evaluatoren, abstrakte Syntaxbäume, Zustandsmaschinen oder Spiellogik – wird mit DOP ausdrucksstärker. In vielen Fällen kann DOP sogar traditionelle Entwurfsmuster wie beispielsweise das Visitor-Pattern ersetzen oder gar komplexe rekursive Methoden vermeiden.

Schauen wir uns zwei konkrete Beispiele an, die diese Vorteile verdeutlichen.

Beispiel 1: State Machine für einen Bestellprozess

Betrachten wir ein Bestellsystem, in dem Bestellungen verschiedene Zustände durchlaufen. Mit DOP können wir dafür eine Zustandsmaschine modellieren und Zustandsübergänge sicher handhaben:

sealed interface OrderState permits Pending, Confirmed, Shipped, Delivered, Cancelled {}

record Pending(String orderId, LocalDateTime createdAt) implements OrderState {}
record Confirmed(String orderId, LocalDateTime confirmedAt, String paymentId) implements OrderState {}
record Shipped(String orderId, LocalDateTime shippedAt, String trackingNumber) implements OrderState {}
record Delivered(String orderId, LocalDateTime deliveredAt, String signature) implements OrderState {}
record Cancelled(String orderId, LocalDateTime cancelledAt, String reason) implements OrderState {}

class OrderProcessor {
    // Process order based on current state - exhaustive and type-safe
    static String getStatusMessage(OrderState state) {
        return switch (state) {
            case Pending(var id, var created) -> 
                "Order " + id + " is pending (created: " + created + ")";
            case Confirmed(var id, var confirmed, var paymentId) -> 
                "Order " + id + " confirmed with payment " + paymentId;
            case Shipped(var id, var shipped, var tracking) -> 
                "Order " + id + " shipped - tracking: " + tracking;
            case Delivered(var id, var delivered, var signature) -> 
                "Order " + id + " delivered and signed by " + signature;
            case Cancelled(var id, var cancelled, var reason) -> 
                "Order " + id + " cancelled: " + reason;
        };
    }
    
    // Zustandsübergänge mit Validierung
    static OrderState confirmOrder(OrderState state, String paymentId) {
        return switch (state) {
            case Pending(var id, var created) -> 
                new Confirmed(id, LocalDateTime.now(), paymentId);
            case Confirmed c -> c; // Already confirmed
            case Shipped _, Delivered _, Cancelled _ -> 
                throw new IllegalStateException("Cannot confirm order in state: " + state);
        };
    }
    
    // Prüfen, ob eine Bestellung abgebrochen werden kann
    static boolean canCancel(OrderState state) {
        return switch (state) {
            case Pending _, Confirmed _ -> true;
            case Shipped _, Delivered _, Cancelled _ -> false;
        };
    }
}

Ein traditioneller OOP-Ansatz des StateMachinePatterns würde folgende Schritte erfordern:

  • Eine abstrakte OrderState-Klasse mit abstrakten Methoden für jede Operation
  • Mehrere konkrete Zustandsklassen, die jeweils alle Methoden implementieren
  • Komplexe Logik für Zustandsübergänge, die über mehrere Klassen verteilt sind
  • Potenzial für Laufzeitfehler, wenn ein Zustand einen Übergang nicht korrekt behandelt
  • Viele Codestellen, die angepasst werden müssen, wenn ein neuer Zustand hinzugefügt wird – jedoch ohne Unterstützung durch den Compiler

Mit DOP stellt der Compiler sicher, dass wir alle Zustände vollständig behandeln. Die gesamte Logik für eine bestimmte Operation befindet sich an einer einzigen Stelle, was das Verständnis und die Wartung erleichtert.

Beispiel 2: JSON Parsing Ohne Traditionelles Visitor Pattern oder Rekursion

JSON-Parsing ist ein klassischer Anwendungsfall für das Visitor-Pattern oder rekursive Methoden. Mit DOP und Pattern Matching können wir dasselbe Ergebnis eleganter erreichen:

/ Modelliere JSON als algebraische Datenstruktur
sealed interface JsonValue {}

record JsonString(String value) implements JsonValue {}
record JsonNumber(double value) implements JsonValue {}
record JsonBoolean(boolean value) implements JsonValue {}
record JsonNull() implements JsonValue {}
record JsonArray(List<JsonValue> elements) implements JsonValue {}
record JsonObject(Map<String, JsonValue> fields) implements JsonValue {}

class JsonProcessor {
    // Konvertiere JSON zu einer String Representation – keine Rekursion erforderlich!
    public String stringify(JsonValue json) {
        return switch (json) {
            case JsonString(var s) -> "\"" + s + "\"";
            case JsonNumber(var n) -> String.valueOf(n);
            case JsonBoolean(var b) -> String.valueOf(b);
            case JsonNull() -> "null";
            case JsonArray(var elements) -> "[" + 
                elements.stream()
                        .map(this::stringify)
                        .collect(Collectors.joining(", ")) + "]";
            case JsonObject(var fields) -> "{" + 
                fields.entrySet().stream()
                      .map(e -> "\"" + e.getKey() + "\": " + stringify(e.getValue()))
                      .collect(Collectors.joining(", ")) + "}";
        };
    }
    
    
    // Finde Wertte im JSON-Pfad (z.B. "user.address.city")
    public JsonValue findAtPath(JsonValue json, String path) {
        String[] parts = path.split("\\.");
        JsonValue current = json;
        
        for (String part : parts) {
            current = switch (current) {
                case JsonObject(var fields) -> 
                    fields.getOrDefault(part, new JsonNull());
                case JsonArray(var elements) -> {
                    try {
                        int index = Integer.parseInt(part);
                        yield index < elements.size() ? elements.get(index) : new JsonNull();
                    } catch (NumberFormatException e) {
                        yield new JsonNull();
                    }
                }
                default -> new JsonNull();
            };
        }
        return current;
    }
}

// Anwendungsbeispiel
class JsonExample {
    public static void main(String[] args) {
        JsonValue user = new JsonObject(Map.of(
            "name", new JsonString("Alice"),
            "age", new JsonNumber(30),
            "address", new JsonObject(Map.of(
                "city", new JsonString("Berlin"),
                "zipCode", new JsonString("10115")
            )),
            "hobbies", new JsonArray(List.of(
                new JsonString("reading"),
                new JsonString("cycling")
            ))
        ));
        
        JsonProcessor processor = new JsonProcessor();
        System.out.println(processor.stringify(user));
        // Output: {"name": "Alice", "age": 30.0, "address": {"city": "Berlin", ...}, ...}
        
        JsonValue city = processor.findAtPath(user, "address.city");
        System.out.println(city); // JsonString[value=Berlin]
    }
}

Vorteile zum Traditionellen Ansatz:

Traditionelle Ansätze würden einen der folgenden Punkte erfordern:

  • Visitor-Pattern: Erstellen eines JsonVisitor-Interfaces mit Methoden für jeden Typ und anschließende Implementierung konkreter Visitors für jede Operation. Dies verteilt die Logik über mehrere Klassen und macht das Hinzufügen neuer Operationen umständlich.
  • Rekursive Methoden mit instanceof: Schreiben von tief verschachtelten if-else-Ketten mit manuellen Typ-Prüfungen und Casts, was fehleranfällig und langatmig ist.

Mit DOP:

  • Die gesamte Logik für eine Operation befindet sich in einem einzigen Switch-Ausdruck
  • Der Compiler stellt die Vollständigkeit sicher – wir können keinen Fall vergessen
  • Pattern Matching eliminiert Boilerplate-Casting und Null-Prüfungen
  • Das Hinzufügen neuer Operationen ist unkompliziert – einfach eine neue Methode mit einem Switch schreiben
  • Der Code drückt die Struktur und Absicht klar aus

Ist Datenorientierte Programmierung der neue Hammer für alle unsere Probleme?

Nein! Datenorientierte Programmierung ist kein Allheilmittel und soll objektorientierte Programmierung nicht ablösen.

Wann man bei OOP bleiben sollte:
OOP ist weiterhin der geeignete Ansatz für Systeme, bei denen Verhalten und Zustand eng gekoppelt sind, wie UI-Frameworks, Spiele-Engines mit komplexem Entitätsverhalten oder Systeme, die zustandsbehaftete Prozesse modellieren. Wenn eure Domäne Objekte umfasst, die ihren Zustand im Laufe der Zeit ändern und vielfältige Schnittstellen besitzen, ist OOP nach wie vor die richtige Wahl.

Wann man DOP verwenden sollte:
DOP glänzt, wenn das primäre Anliegen die Modellierung und Transformation von Datenstrukturen ist. Es ist besonders nützlich in funktionalen Pipelines, Parsern, Compilern, Konfigurationssystemen und bei datenzentrierter Geschäftslogik – in der Regel in kleineren Systemen.

Der hybride Ansatz:
In der Praxis profitieren die meisten modernen Java-Anwendungen von einem hybriden Ansatz. DOP wird für die Datenmodellierung und Transformationslogik eingesetzt, während OOP für zustandsbehaftete Komponenten, Frameworks und Infrastrukturcode verwendet wird. Der Schlüssel liegt darin, das richtige Werkzeug für jede Problemdomäne zu wählen.

Die Weiterentwicklung von Java mit Records, sealed Interface/Klassen und Pattern Matching gibt uns die Flexibilität, das am besten geeignete Paradigma für jeden Teil unseres Systems zu wählen. Es geht nicht darum, einen Ansatz durch einen anderen zu ersetzen – es geht darum, unseren Werkzeugkasten zu erweitern und fundierter Designentscheidungen zu treffen.

Zusammenfassung

Datenorientierte Programmierung stellt eine bedeutende Weiterentwicklung in der Art und Weise dar, wie wir Java-Code schreiben können. Durch die Kombination von sealed Klassen/Interfaces, Records und Pattern Matching erhalten wir ein leistungsstarkes Werkzeug zur klaren und sicheren Modellierung von Daten.
Die vier Prinzipien – reine Daten modellieren, Unveränderlichkeit sicherstellen, an den Grenzen validieren und illegale Zustände zu verhindern – ermöglichen robustere und wartbare Systeme.

Da sich Java kontinuierlich weiterentwickelt, werden neue Features wie Withers und ein erweitertes Pattern Matching DOP noch ergonomischer machen. Die Sprache eröffnet uns neue Wege, alte Probleme zu überdenken. Es lohnt sich zu erkunden, wie diese Techniken euren Code verbessern können.

Ressourcen

  1. Data Oriented Programming in Java by Brian Goetz and Daniel Bryant
  2. Data-Oriented Programming in Java by Gavin Bierman
  3. Data-Oriented Programming in Java – Version 1.1 by Nikolai Parlog
  4. Harnessing Java 21 for Data Oriented Programming by Ken Kousen
  5. Data Oriented Programming in Java 21 by Nicolai Parlog
Total
0
Shares
Previous Post

Ein Vaadin-Starterprojekt mit klarem Fokus

Next Post

Mehr als nur eine Konferenz für Java-Enthusiasten | JCON 2026

Related Posts