La fonctionnalité d’enregistrement est arrivée dans la dernière version LTS, Java 17 ! Les enregistrements permettent de créer une classe immuable sans passe-partout. C’est génial! La question est : comment pouvons-nous l’utiliser ? En général, nous avons vu quelques échantillons avec DTO, mais nous pouvons faire plus que cela. Dans ce didacticiel et cette vidéo, nous allons explorer les capacités de conception avec un record dépassant le DTO.
DTO
Nous ne nous y attarderons pas ici, mais il convient de mentionner qu’il s’agit d’un bon échantillon d’enregistrement mais pas d’un cas unique.
Peu importe que vous utilisiez Spring, MicroProfile ou Jakarta. Actuellement, nous avons plusieurs exemples de cas que je vais énumérer ci-dessous :
Objets de valeur ou types immuables
Dans la DDD, value
les objets représentent un concept de votre domaine de problème. Ces classes sont immuables, telles que Money
, email
etc. Ainsi, une fois que les deux enregistrements d’objets de valeur sont fermes, vous pouvez les utiliser.
Dans notre premier exemple, nous allons créer un email
qui n’a besoin que d’une validation :
public record Email (String value) {
}
Comme avec n’importe quel value
objet, vous pouvez ajouter des méthodes et un comportement, mais le résultat doit être une instance différente. Imaginez que nous allons créer un Money
type, et nous voulons créer le add
opération. Ainsi, nous allons ajouter la méthode pour vérifier s’il s’agit de la même devise, puis aboutir à une nouvelle instance :
public record Money(Currency currency, BigDecimal value) {
Money add(Money money) {
Objects.requireNonNull(money, "Money is required");
if (currency.equals(money.currency)) {
BigDecimal result = this.value.add(money.value);
return new Money(currency, result);
}
throw new IllegalStateException("You cannot sum money with different currencies");
}
}
Les Money
était un exemple, principalement parce que Java a une spécification avec JavaMoney et une célèbre bibliothèque, Joda-Money, où vous pouvez l’utiliser. Le point est lorsque vous devez créer un Value
objet ou un enregistrement de type immuable qui peut parfaitement s’y adapter.
Entités immuables
Mais attendez? Vous avez dit entités immuables ? Est-ce possible? Ce n’est pas habituel, mais cela arrive, comme lorsque l’entité détient un point de transition historique.
Une entité peut-elle être immuable ?
Si vous vérifiez la définition d’Evan d’une entité au chapitre 5 :
Une entité est tout ce qui a une continuité à travers un cycle de vie et des distinctions indépendantes des attributs essentiels à l’utilisateur de l’application.
L’entité n’est pas sur le point d’être mutable ou non, mais elle est liée au domaine ; ainsi, nous pouvons avoir des entités immuables, mais encore une fois, ce n’est pas habituel. Il y a une discussion sur Stackoverflow à propos de cette question.
Créons une entité, Book
lorsque cette entité a un ID
, title
et release
comme un an. Que se passe-t-il si vous souhaitez éditer un livre ? Nous ne le faisons pas : nous devons créer une nouvelle édition. Par conséquent, nous ajouterons également le edition
champ.
public record Book(String id, String title, Year release, int edition) {}
Ok, mais nous avons aussi besoin de validation. Sinon, cela book
aura des données incohérentes à ce sujet. Il n’est pas logique d’avoir des valeurs nulles sur le id
, title
et release
en édition négative. Avec un enregistrement, nous pouvons utiliser le constructeur compact et y mettre des validations :
public Book {
Objects.requireNonNull(id, "id is required");
Objects.requireNonNull(title, "title is required");
Objects.requireNonNull(release, "release is required");
if (edition < 1) {
throw new IllegalArgumentException("Edition cannot be negative");
}
}
Nous pouvons écraser equals
, hashCode
et toString
méthodes si nous le souhaitons. En effet, écrasons le equals
hashCode
des contrats pour opérer sur id
champ:
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
Book book = (Book) o;
return Objects.equals(id, book.id);
}
@Override
public int hashCode() {
return Objects.hashCode(id);
}
Pour faciliter la création de cette classe ou lorsque vous avez des objets plus complexes, vous pouvez soit créer une méthode de fabrique, soit définir builders
. Le code ci-dessous montre le builder
création sur le book
méthode d’enregistrement :
Book book = Book.builder().id("id").title("Effective Java").release(Year.of(2001)).builder();
À la fin de notre entité immuable avec un enregistrement, nous inclurons également la méthode de changement, où nous devons changer le livre en une nouvelle édition. Dans la prochaine étape, nous verrons la création de la deuxième édition de Effective Java
. Ainsi, nous ne pouvons pas changer le fait qu’il y ait eu une première édition de ce livre une fois; cette partie historique fait partie de notre activité de bibliothèque.
Book first = Book.builder().id("id").title("Effective Java").release(Year.of(2001)).builder();
Book second = first.newEdition("id-2", Year.of(2009));
Actuellement, JPA ne peut pas prendre en charge immuable pour des raisons de compatibilité, mais nous pouvons l’explorer sur des API NoSQL telles que Eclipse JNoSQL et Spring Data MongoDB.
Nous avons couvert bon nombre de ces sujets; par conséquent, passons à un autre modèle de conception pour représenter la forme de notre conception de code.
Mise en œuvre de l’État
Il y a des circonstances où nous devons implémenter un flux ou un état à l’intérieur du code. Le modèle de conception d’état explore un contexte de commerce électronique dans lequel nous avons une commande et nous devons conserver le flux chronologique d’une commande. Naturellement, nous voulons savoir quand il a été demandé, livré et finalement reçu de l’utilisateur.
La première étape est la création de l’interface. Pour le rendre lisse, nous utiliserons String
représenter products
mais vous savez que nous aurons besoin d’un objet entier pour cela :
public interface Order {
Order next();
List<String> products();
}
Avec cette interface prête à l’emploi, créons une implémentation qui suit ses flux et renvoie les produits. Nous voulons éviter toute modification des produits. Ainsi, nous écraserons le products
méthodes de l’enregistrement pour produire une liste en lecture seule.
public record Ordered(List<String> products) implements Order {
public Ordered {
Objects.requireNonNull(products, "products is required");
}
@Override
public Order next() {
return new Delivered(products);
}
@Override
public List<String> products() {
return Collections.unmodifiableList(products);
}
}
public record Delivered(List<String> products) implements Order {
public Delivered {
Objects.requireNonNull(products, "products is required");
}
@Override
public Order next() {
return new Received(products);
}
@Override
public List<String> products() {
return Collections.unmodifiableList(products);
}
}
public record Received(List<String> products) implements Order {
public Received {
Objects.requireNonNull(products, "products is required");
}
@Override
public Order next() {
throw new IllegalStateException("We finished our journey here");
}
@Override
public List<String> products() {
return Collections.unmodifiableList(products);
}
}
Nous avons l’État mis en œuvre; changeons le Order
interface. Tout d’abord, nous allons créer une méthode statique pour démarrer une commande. Ensuite, pour nous assurer que nous n’aurons pas de nouvel état intrus, nous bloquerons l’implémentation de l’état du nouvel ordre et n’autoriserons que ceux que nous avons ; par conséquent, nous utiliserons le sealed interface
fonctionnalité.
public sealed interface Order permits Ordered, Delivered, Received {
static Order newOrder(List<String> products) {
return new Ordered(products);
}
Order next();
List<String> products();
}
Nous l’avons créé! Nous allons tester le code avec une liste de produits. Comme vous pouvez le voir, notre flux explore les capacités des enregistrements.
List<String> products = List.of("Banana");
Order order = Order.newOrder(products);
Order delivered = order.next();
Order received = delivered.next();
Assertions.assertThrows(IllegalStateException.class, () -> received.next());
L’état avec une classe immuable permet de penser à des moments transactionnels, comme une entité, ou de générer un événement sur une architecture événementielle.
Vidéo
Découvrez plus d’informations sur la vidéo pour en savoir plus sur l’enregistrement :