3 Ways That You Can Operate Record Beyond DTO [Video]
The record feature has arrived in the latest LTS version, Java 17! But how can we use it? This post explores design capabilities with a record exceeding DTO.
Join the DZone community and get the full member experience.
Join For FreeThe record feature has arrived in the latest LTS version, Java 17! Records allow the making of an immutable class without a boilerplate. That is awesome! The question is: how can we use it? In general, we saw a couple of samples with DTO, but we can do more than that. In this tutorial and video, we'll explore design capabilities with a record exceeding DTO.
DTO
We won't focus on it here, but it is worth mentioning that it is a good sample of record but not a unique case.
It does not matter if you use Spring, MicroProfile, or Jakarta. Currently, we have several samples cases that I'll list below:
Value Objects or Immutable Types
In the DDD, value
objects represent a concept from your problem domain. Those classes are immutable, such as Money
, email
, etc. So, once both value objects records are firm, you can use them.
In our first sample, we'll create an email
that needs validation only:
public record Email (String value) {
}
As with any value
object, you can add methods and behavior, but the result should be a different instance. Imagine we'll create a Money
type, and we want to create the add
operation. Thus, we'll add the method to check if those are the same currency and then result in a new 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");
}
}
The Money
was a sample, mainly because Java has a specification with JavaMoney and a famous library, Joda-Money, where you can use it. The point is when you need to create a Value
object or an immutable type record that can fit perfectly on it.
Immutable Entities
But wait? Did you say immutable entities? Is that possible? It is not usual, but it happens, such as when the entity holds a historic transitional point.
Can an entity be immutable?
If you check Evan's definition of an entity in Chapter 5:
An entity is anything that has continuity through a life cycle and distinctions independent of attributes essential to the application's user.
The entity is not about be mutable or not, but it is related to the domain; thus, we can have immutable entities, but again, it is not usual. There is a discussion at Stackoverflow about this question.
Let's create an entity, Book
, where this entity has an ID
, title
, and release
as a year. What happens if you want to edit a book? We don't: we need to create a new edition. Therefore, we'll also add the edition
field.
public record Book(String id, String title, Year release, int edition) {}
Ok, but we also need validation. Otherwise, this book
will have inconsistent data on it. It does not make sense to have null values on the id
, title
, and release
as a negative edition. With a record, we can use the compact constructor and put validations on it:
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");
}
}
We can overwrite equals
, hashCode
, and toString
methods if we wish. Indeed, let's overwrite the equals
hashCode
contracts to operate on the id
field:
@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);
}
To make it easier to create this class or when you have more complex objects, you can either create a factory method or define builders
. The code below shows the builder
creation on the book
record method:
Book book = Book.builder().id("id").title("Effective Java").release(Year.of(2001)).builder();
At the end of our immutable entity with a record, we'll also include the change method, where we need to change the book to a new edition. In the next step, we'll see the creation of the second edition of Effective Java
. Thus, we cannot change the fact that there was a first edition of this book once; this historical part is part of our library business.
Book first = Book.builder().id("id").title("Effective Java").release(Year.of(2001)).builder();
Book second = first.newEdition("id-2", Year.of(2009));
Currently, JPA cannot support immutable for compatibility reasons, but we can explore it on NoSQL APIs such as Eclipse JNoSQL and Spring Data MongoDB.
We covered many of those topics; therefore, let's move on to another design pattern to represent the form of our code design.
State Implementation
There are circumstances where we need to implement a flow or a state inside the code. The state design pattern explores an e-commerce context where we have an order, and we need to keep the chronological flow of an order. Naturally, we want to know when it was requested, delivered, and finally received from the user.
The first step is interface creation. To make it smooth, we'll use String
to represent products
, but you know we'll need an entire object for it:
public interface Order {
Order next();
List<String> products();
}
With this interface ready for use, let's create an implementation that follows its flows and returns the products. We want to avoid any change to the products. Thus, we'll overwrite the products
methods from the record to produce a read-only list.
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);
}
}
We have the state implemented; let's change the Order
interface. First, we'll create a static method to start an order. Then, to ensure that we won't have a new intruder state, we'll block the new order state implementation and only allow the ones we have; therefore, we'll use the sealed interface
feature.
public sealed interface Order permits Ordered, Delivered, Received {
static Order newOrder(List<String> products) {
return new Ordered(products);
}
Order next();
List<String> products();
}
We made it! We'll test the code with a list of products. As you can see, we have our flow exploring the capabilities of records.
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());
The state with an immutable class allows you to think about transactional moments, such as an entity, or generate an event on an event-driven architecture.
Video
Check out more video info to know more about the record:
Opinions expressed by DZone contributors are their own.
Comments