Tips To Write Better DTOs in Java
Omar Ismail
Senior Software Engineer @ Digitinary | Java 8 Certified? | Spring & Spring Boot?????? | AWS? | Microservices ?? | RESTFul Apis & Integrations ?? FinTech ?? | Open Banking ?? | Digital Payments and Transformation??
Thanks to the original writer and original content :
https://medium.com/javarevisited/not-so-obvious-tips-to-write-better-dtos-in-java-c6116895b180
How to write DTOs that simplify your code.
Today, applications tend to be more distributed. We need to write more code to connect to other services, and still try to keep it simple.
To use data from an external service, we usually convert a JSON payload to a Data Transfer Object (DTO).?The code that handles DTOs quickly becomes complex,?but a few tips can help. We can write DTOs that are easier to interact with, DTOs that make the client code easier to write, and easier to read. Used together, these tips help to keep it simple.
DTO serialization from the manual
Let’s start with the typical way to work with JSON. Here is a JSON structure. It represents a Regina pizza.
To use this data in my application, I create a simple DTO named?
PizzaDto
PizzaDto
?is a?Plain Old Java Object : an object with properties, getters, setters, and that’s all. It mirrors the JSON structure, so the conversion between object and JSON is just a one-liner. Here is an example with the?Jackson library :
The conversion is straightforward. So, what’s the problem?
In real life, DTOs can be quite complex. The code to create and initialize the DTO can be huge: sometimes dozens of code lines. Sometimes more. That’s a problem because complex code contains more bugs, and it’s less responsive to change.
My first trip to simplify DTO creation is to use an?immutable? DTO: a DTO that cannot be modified after creation.
It may sound odd if you’re not familiar with the idea, so let’s have a focus on the topic.
Create immutable DTOs
Simply put, an object is immutable when its state cannot change after construction.
Let’s rewrite the?
PizzaDto
?to make it?immutable .
The immutable version has no setter. All the properties are final and must be initialized at construction.
As you can see, the ingredient list is not stored as-is. Instead, I use?
List.copyOf()
?to keep an?unmodifiable? copy of the input. This prevents the clients to modify the ingredients stored in the DTO.
This is important, because a Regina pizza without mushrooms is definitely NOT a Regina pizza.
More seriously, Joshua Bloch, the author of?Effective Java , gives this recommendation to create immutable classes:
“If your class has any fields that refer to mutable objects, ensure that clients of the class cannot obtain references to these objects.” Joshua Bloch
If any property of your DTO is mutable, you need to make?defensive copies. With defensive copies, your DTO is protected from external modifications.
Post Publish edit:?Starting with Java 16,?records ?offer a more concise way to create immutable classes.
OK. Now we have an immutable DTO. But how does it simplify the code?
Benefits of immutability
Immutability brings many benefits, but here is my favorite: immutable variables are?side effect-free.
Let’s see this through an example. There’s a bug in this snippet:
After running this code,?
pizza
?doesn't have the expected state. Which line caused the issue?
We are going to consider 2 answers : first with a mutable variable, and then, with an immutable one.
First answer, with a?mutable?pizza.?
pizza
?is created by?
make()
, but it can be modified within?
verify()
?and?
serve()
. So, the bug can come from any of the 3 lines.
Now, second answer, with an?immutable?pizza.?
make()
?returns a pizza, but?
verify()
?and?
serve()
?cannot modify it. The issue can only come from?
make()
. Here, the scope to invest is far smaller. The bug is easier to find.
When we use immutable variables, debugging is easier. But there’s more.
When a pizza is invalid,?
verify()
?probably throws an exception to interrupt the process. Let's change this. We want?
领英推荐
verify()
?to fix invalid pizzas.
Since the pizza is immutable,?
verify()
?can’t just fix it. It has to create and return a modified pizza, and the client code must be adapted:
In this new version, it’s obvious that?
verify()
?returns a new, fixed pizza. Immutability makes your code more explicit. It becomes easier to read and easier to evolve.
You may not know, but we already use immutable objects every day.?
java.lang.String
,?
java.math.BigDecimal
,?
java.io.File
?are immutable. Immutability offers?many other advantages . In his?Effective Java , Joshua Bloch simply recommends to “minimize mutability”.
Immutable classes are easier to design, implement, and use than mutable classes. They are less prone to error and are more secure. Joshua Bloch
Now, the interesting question is: can we use it for our DTOs?
Immutable DTOs… Does it make sense?
The aim of a DTO is to carry data between processes. It is initialized, and then, its state should not evolve. Either it will be?serialized to JSON , or it will be used by a client. This makes?immutability?a natural fit. An immutable DTO would carry data between processes, with the warranty it’s unaltered.
So, why did I first write a mutable?
PizzaDto
, and not an immutable one? The thing is, I was pretty sure my JSON library required getters and setters on the DTO.
Turns out I was wrong!
Immutable DTOs with Jackson
Jackson ?is the most common JSON library in Java.
When your DTO has getters and setters,?Jackson can map the object to JSON ?without any extra configuration. But with immutable objects, Jackson needs a little help. It needs to know how to construct the object.
The object’s constructor must be annotated with?
@JsonCreator
, and each argument with?
. Let’s add these annotations on the DTO’s constructor.
And that’s all. We have an immutable DTO, that Jackson can convert to JSON, and back to object.
Immutable DTOs with Gson and Moshi
With these libraries, it's even simpler to convert JSON to an immutable DTO, because they don't need any extra annotations.
But, why does Jackson require annotations, when Gson and Moshi don't?
It's not magic. Actually, when?Gson? and Moshi generate an object from JSON, they create and initialize it by reflection. Finally, they just don't use constructors.
I'm not a big fan of this approach. It’s misleading because a developer may put some logic in a constructor and never know it is not called. In comparison, I find?Jackson? much safer.
Avoid null values
There’s another good point with Jackson. If we put some logic in the constructor, it will always be called, whether the DTO is created by the application code, or generated from JSON.
We can take advantage of this and?avoid null values . We can improve the constructor to initialize fields with non-null values.
In the snippet below, fields are initialized with empty values when the input is null.
Most of the time, empty and?
null
?don't make a difference. If we replace null values with empty ones, clients can use DTO properties without first checking if it's not null. Plus, it lowers the chances to get?NullPointerExceptions .
With this tip, you write less code, and you increase robustness. How can we do better?
Last but not least, create DTO with Builders
I use another tip to simplify DTO initialization. For each DTO, I create a?Builder? companion. The builder provides a fluent API to facilitate the DTO initialization.
This is an example to create a PizzaDto with a Builder:
With complex DTOs, builders make the code more expressive. This pattern is so brilliant that Joshua Bloch almost starts his?Effective Java ?with it.
This client code is easy to write and, more importantly, to read. Joshua Bloch
How does it work? The builder object simply stores values, until we call?
build()
, which actually creates the desired object with the stored values.
Here is an example for the?
PizzaDto
Some people use?Lombok ?to create builders at compile time. This keeps DTOs simple.
I prefer to generate the builder code with the?Builder generator IntelliJ plugin . Then, I can add method overloads, like I did in the previous snippet. The builder is more flexible and the client code is leaner.
Senior Java Developer
2 年Thanks for posting