Kolumne: EnterpriseTales

Record-Type: Value Objects werden endlich Java-native

Arne Limburg, Hendrik Müller

© S&S_Media

Value Objects sind einer der fundamentalen Building Blocks in Domain-driven Design. Sie in Java zu erstellen, erforderte bisher allerdings einigen Boilerplate-Code. Das ändert sich mit Java 15: Es wird ein neues Sprachkonstrukt eingeführt – und zwar die Records. Und diese erfüllen alle technischen Anforderungen zur einfachen Umsetzung von Value Objects.

Aus objektorientiertem Domain-driven Design kennen wir Value Objects (z. B. [1]). Sie stellen die Werte innerhalb der Domänenmodellierung dar: Sie haben im Gegensatz zu Entitäten keine Identität und können sich daher nicht über die Zeit hinweg verändern. Da Value Objects keine Identität haben, erfolgt der Vergleich zweier Value Objects immer über alle enthaltenen Werte. Sowohl aus logischer als auch aus technischer Sicht ergibt sich daraus, dass Value Objects unveränderlich, also immutable sind.

Genauso wie für Entitäten gilt auch für Value Objects der Anspruch, immer einen validen Zustand zu repräsentieren. Entsprechend muss dies zum Erstellungszeitpunkt, also im Konstruktor, sichergestellt werden. Auch viele der vom JDK mitgelieferten Datentypen sind unveränderlich, so z. B. die Klassen java.lang.String, java.math.BigInteger und java.math.BigDecimal. Instanzen dieser Klassen können nicht verändert werden. Stattdessen geben Methoden wie BigInteger#add das Ergebnis in Form einer neuen Instanz zurück.

Vorteile von Immutability

Dass Value Objects auch unabhängig von Domain-driven Design sinnvoll sind, beschreibt Martin Fowler in seinem Blog [2]. Das Konzept Immutability hat sich außerhalb der objektorientierten Programmierung schon länger etabliert. So sind die primitiven Datentypen, wie beispielsweise Zahlen oder Strings, in den meisten anderen Programmiersprachen auch unveränderlich, und gerade in nebenläufigen Systemen senkt shared mutable State (also Zustand, den verschiedene Teile des Systems parallel ändern können) nicht gerade die Komplexität. Das wird jedem spätestens dann klar, wenn er mit relationalen Datenbanken arbeitet. Dort gibt es das Konzept der Datenbanktransaktionen, um durch Isolation der Herausforderung zu begegnen, dass der shared State parallel verändert werden könnte. Das geschieht, indem die Änderungen (zumindest logisch) nacheinander ausgeführt werden.

Ein weiterer Vorteil der Verwendung von unveränderlichen Werten ist, dass sich durch sie in der Regel baumartige Strukturen ergeben und keine zyklischen Objektgraphen. Das liegt daran, dass sich Rückwärtsreferenzen durch die Unveränderlichkeit normalerweise nicht etablieren lassen. Allerdings gibt es in einigen Programmiersprachen (z. B. Haskell) Sprachkonstrukte, die dennoch zyklische, unveränderbare Strukturen ermöglichen. Durch Baumstrukturen wird die Serialisierung (z. B. nach JSON oder XML) deutlich vereinfacht.

Unveränderliche Datenstrukturen führen aber nicht in jeder Situation zu einer Vereinfachung. Benötigt man gerade besagte zyklische Abhängigkeiten in seinem Objektmodell, muss deutlich mehr Gehirnschmalz in die Modellierung gesteckt werden.

Nicht zuletzt haben die meisten Enterprise-Systeme in der Realität einen Zustand, der sich über die Zeit ändert, was durch komplette Immutability nicht abgebildet werden kann.

Values in funktionaler Programmierung

Ohne jetzt zu akademisch zu werden, kann man sagen, dass die funktionale Programmierung die seiteneffektfreie oder „pure“ Funktion in den Fokus der Modellierung stellt. Der Begriff Funktion ist etwas verwässert im Zusammenhang mit Programmiersprachen. Eine pure Funktion, wie sie auch im mathematischen Sinne verwendet wird, liefert für die gleichen Eingaben immer dasselbe Ergebnis. Entsprechend gibt es auch keine Veränderung von externem Zustand (also Zustand, der über den Funktionsaufruf hinweg existiert). Das steht im Gegensatz dazu, wie man es aus der imperativen Programmierung und damit auch den meisten objektorientierten Programmiersprachen gewohnt ist. Es gibt diverse Vor- und Nachteile dieser funktionalen Ansätze, auf die wir hier nicht im Detail eingehen wollen. Der große Vorteil für den Programmierer ist aber vor allem: „It’s easy to reason about“. Wenn es keinen verstecken Zustand und keine globalen Variablen gibt, die innerhalb einer Funktion geändert werden können, wird der Code deutlich verständlicher. Man kann dann über den Inhalt einer Funktion reden, ohne einen größeren Kontext betrachten zu müssen. Alles, was Relevanz für eine Berechnung hat, wird explizit mitgegeben. Daraus folgt auch, dass Ein- und Ausgabewerte nicht verändert werden dürfen. Wenn sie also von vornherein unveränderlich oder immutable sind, ist diese Garantie automatisch gegeben.

Funktionale Programmierung in der Enterprise-Welt

Das funktionale Paradigma hat mittlerweile auch in den Enterprise-Architekturen seinen festen Platz gefunden. Programmiersprachen wie F#, Clojure, Scala oder auch Erlang stellen funktionale Programmierung in den Fokus. Und auf einer architekturellen Ebene erfreuen sich Stateless Services und die Lambdaarchitektur wachsender Beliebtheit.

Um das funktionale Paradigma sinnvoll umsetzen zu können, haben die meisten dieser Programmiersprachen und -konzepte auch immer Konstrukte für immutable State. So gibt es etwa in Scala die Case Classes und in Kotlin die Data Classes. In Java muss man sich bisher mit Libraries wie Lombok begnügen.

Auch Java hat in den letzten Versionen immer mehr Features bekommen, die es im Lager der funktionalen Programmiersprachen schon lange gibt: Lokale Typinferenz (var) und Lambdas stechen dabei besonders hervor. Was bisher fehlte, war ein Sprachkonstrukt für immutable State. Dieses steht jetzt mit Java 15 vor der Tür: Records.

Java Records

Ab Java 14 sind Records als Preview enthalten. Sie stellen eine weniger verbose Möglichkeit zur Verfügung, Daten zu modellieren. Code sagt mehr als tausend Worte. Die beiden Codebeispiele sind nahezu äquivalent: einmal in der neuen Record-Syntax (Listing 1) und einmal klassisch mit Klassen (Listing 2).

// record syntax
record Record(int a, int b) {
}
// class syntax
public final class RecordObject {
  final int a;
  final int b;
 
  public RecordObject(int a, int b) {
    this.a = a;
    this.b = b;
  }
 
  public int a() {
    return a;
  }
 
  public int b() {
    return b;
  }
 
  public boolean equals(Object o) {
    if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false;
    RecordObject other = (RecordObject)o;
    return a == other.a && b == other.b;
  }
 
@Override
  public int hashCode() {
    return Objects.hash(a, b);
  }
 
@Override
  public String toString() {
    return "RecordObject[" +
           "a=" + a +
           ", b=" + b +
           ']';
  }
}

Records als Value Objects

In der zugehörigen Spezifikation JEP 359 [3] werden Records folgendermaßen beschrieben: „Records can be considered a nominal form of tuples.“ Records sind also benannte (und damit typisierte) Tupel. Entsprechend dieser Spezifikation scheint es zunächst offensichtlich, dass sie sich gut dazu eignen, Value Objects zu modellieren. Das ist auch nicht falsch, aber auch nicht unbedingt immer richtig, bzw. nicht immer ganz so einfach. Records lassen sich sehr gut als fachliche Wrapper für primitive Datentypen einsetzen. Ein Objekt vom Typ CustomerId hat eine größere Aussagekraft als eine Kundennummer in Form eines Strings.

Wenn es komplizierter wird, muss man bedenken, dass Records Daten explizit nach außen sichtbar machen. Es gilt also auch das Konzept von Information Hiding explizit nicht. Das stimmt zwar nicht zu 100 Prozent, weil es immer noch eine Indirektion über einen Component Accessor (quasi einen Getter, allerdings nicht nach JavaBeans-Konvention) gibt, um auf den inneren State zuzugreifen, aber kommt der Sache schon sehr nahe. Die innere Struktur bildet damit die Grundlage für das Public Interface. Entsprechend ist eine komplexe Implementierung immer komplett von außen sichtbar. Ein Paradigma der Objektorientierung, nämlich eben besagtes Information Hiding, wird dadurch konterkariert. Die meisten Anwendungsfälle wird man trotzdem sehr gut abbilden können, allerdings sind althergebrachte OO-Modellierungsstrategien eventuell nicht eins zu eins auf Records anwendbar. So lässt sich von einem Record z. B. nicht ableiten. Außerdem gibt es automatisch den kanonischen Konstruktor, der alle Felder als Parameter bekommt. Dadurch ist es nicht möglich, einzelne Parameter anderweitig (z. B. durch Berechnung) zu ermitteln oder etwa im Konstruktor einen String zu parsen, um den Record zu erstellen. Eine solche Implementierung ist zwar weiterhin möglich, den kanonischen Konstruktor gibt es aber immer zusätzlich. Auch der Name der Zugriffsmethoden lässt sich nicht beeinflussen – er stimmt mit dem Feldnamen überein. Records weichen somit vom JavaBean-Standard ab, weil das get vor dem Methodennamen fehlt.

Die Felder der Records können normale Java-Objekte sein. Daraus resultiert, dass Records nicht deeply immutable sind: Der Zustand der innenliegenden Java-Objekte könnte ja weiterhin verändert werden. Dadurch ist es dann auch wieder möglich, zirkuläre Objektgraphen zu erzeugen, die Records enthalten. Das Erstellen der oben erwähnten unveränderlichen Baumstrukturen aus der funktionalen Programmierung wird mit Records also zwar ermöglicht, aber nicht sichergestellt.

Records und Enterprise Java

Wie passen Records in die Enterprise-Java-Welt? Zunächst einmal sind sie architektonisch eine sinnvolle Ergänzung. An einigen Stellen ist es immer sinnvoll, echte Value Objects (also immutable Objects) zu verwenden. Das gilt z. B. für den Payload eines POST Requests oder auch für ein Data Transfer Object zwischen der Schicht, die die Fachlogik enthält, und der Webschicht. Hier erleichtern Records einem das Leben erheblich, weil der oben erwähnte Boilerplate-Code wegfällt. Diese Objekte enthalten normalerweise auch keine Domänenlogik, sodass Records in ihrer einfachsten Form verwendet werden können.

Natürlich wird es einige Zeit dauern, bis die verschiedenen Frameworks das neue Java-Feature adaptieren. Dabei dürfte es einigen leichter fallen als anderen. Die Serialisierung und Deserialisierung nach JSON über den JSON-B-Standard sind z. B. bereits heute möglich (getestet mit Apache Johnzon [4]). Dabei muss der kanonische Konstruktor public sein und mit der Annotation @JsonbCreator markiert werden. Darüber wird signalisiert, dass dieser Konstruktor zur Objekterzeugung verwendet werden soll.

Etwas komplizierter dürfte die Integration mit Bean Validation werden. Zwar sieht die Spezifikation hier bereits die Möglichkeit von Constructor Validation vor, diese muss in der Praxis aber immer manuell angestoßen werden. Integrierende Spezifikationen wie z. B. JAX-RS verwenden aber nach wie vor Property Validation. Das würde im Fall von Records heißen, dass sie zunächst ungültig erzeugt werden müssten, um dann per Bean Validation validiert zu werden. Für die ungültige Erzeugung würde aber der kanonische Konstruktor verwendet. Dieser dürfte dann aber nicht selbst validieren, weil das zu einem Laufzeitfehler führen würde.

Noch größere Änderungen stehen der JPA-Spezifikation bevor. Hier sind Value Objects zwar grundsätzlich schon als @Embeddable vorgesehen. Eine Erzeugung über einen Konstruktor ist aber noch nicht möglich. Das dahingehende Proposal hat Oliver Drotbohm (damals Gierke) bereits 2011 bei der JPA Expert Group eingereicht. Es wurde allerdings abgelehnt [5]. Bisher ist in JPA daher nur das direkte Befüllen der Felder eines Objekts oder das Befüllen über Setter-Methoden vorgesehen. Beides wäre bei Records nicht möglich.

Fazit

Mit Java 15 bekommt Java Records als neues Sprachfeature, ein Konstrukt, um Value Objects einfacher zu realisieren. Teilweise kann dieser Typ auch direkt mit Enterprise-Java-Standards verwendet werden (z. B. bei JSON-B). Andere Standards wie JPA müssen ihre Spezifikationen noch anpassen, damit er verwendet werden kann.

Records sind allerdings nicht deeply immutable, d. h. man kann in einen Record wiederum ein änderbares Java-Objekt stecken. Deshalb kann man mit Records nicht sicherstellen, dass die aus der funktionalen Programmierung als vorteilhaft bekannten unveränderlichen Baumstrukturen erreicht werden. Allerdings vereinfachen Records das Erstellen solcher Strukturen.

Software, die sich auf shared mutable State verlässt, also auf eine geteilte Datenstruktur, die an unterschiedlichen Stellen des Programmcodes geändert wird, ist schwer wartbar. Mit Records wird es leichter, Software zu schreiben, die weniger davon enthält, oder, wie es im JEP beschrieben wird: „It should be easy, clear, and concise to declare shallowly-immutable, well-behaved nominal data aggregates.“ Es soll mit der Spezifikation einfach, klar und kurz möglich sein, (zumindest) auf oberster Ebene unveränderliche, sich wohlverhaltende, benannte (und damit typsichere) Datenaggregate zu erstellen.

Dieser Vorteil kann bereits jetzt in Enterprise Java z. B. für die Konvertierung von und nach JSON oder beim Einsatz als DTO verwendet werden. Bis Records aber in allen Jakarta-IEE-Spezifikationen uneingeschränkt nutzbar sind, wird es erfahrungsgemäß noch eine Weile dauern. In diesem Sinne: Stay Tuned!

Geschrieben von
Arne Limburg
Arne Limburg
Arne Limburg ist Softwarearchitekt bei der open knowledge GmbH in Oldenburg. Er verfügt über langjährige Erfahrung als Entwickler, Architekt und Consultant im Java-Umfeld und ist auch seit der ersten Stunde im Android-Umfeld aktiv.
Hendrik Müller
Hendrik Müller
Hendrik Müller ist Enterprise Developer bei der OPEN KNOWLEDGE GmbH in Oldenburg. Mit dem Schwerpunkt auf Webtechnologien begleiten ihn momentan Angular und Web Components durch den Tag. Abseits davon beschäftigt er sich leidenschaftlich mit dem Design von Programmiersprachen.
Kommentare

Hinterlasse einen Kommentar

avatar
4000
  Subscribe  
Benachrichtige mich zu: