Solution
In order to solve the inconvenience of secondary "modification" (creating a new immutable object based on another immutable object) of complex immutable objects in the current JVM ecosystem, some immutable object frameworks were born:
Jimmer is an ORM framework, and handling deep data structures is a core issue for ORM, so Jimmer must do similar work.
Jimmer needs to provide elegant dual language support for both Java and Kotlin at the same time. None of the above solutions can meet this requirement, so Jimmer did not use any of them, but chose to transplant immer from the JavaScript/TypeScript field.
Next, we will demonstrate the power of immutable objects transplanted from immer in three steps:
-
Define immutable types
-
Create an immutable data structure from scratch
-
Based on an existing data structure, create a new data structure according to some desired modifications.
This last step is where the core value of immer lies. Please pay close attention.
1. Define immutable type
To demonstrate this feature, there is no need for the @Entity
annotation on ORM entities, the non-ORM @Immutable
is sufficient.
- Java
- Kotlin
package yourpackage;
import java.util.List;
import org.babyfish.jimmer.Immutable;
@Immutable
public interface TreeNode {
String name();
List<TreeNode> childNodes();
}
package yourpackage
import org.babyfish.jimmer.Immutable
@Immutable
interface TreeNode {
val name: String
val childNodes: List<TreeNode>
}
2. Create data structure from scratch
- Java
- Kotlin
TreeNode treeNode = Immutables.createTreeNode(root -> {
root.setName("Root").addIntoChildNodes(food -> {
food
.setName("Food")
.addIntoChildNodes(drink -> {
drink
.setName("Drink")
.addIntoChildNodes(cocacola -> {
cocacola.setName("Cocacola");
})
.addIntoChildNodes(fanta -> {
fanta.setName("Fanta");
});
;
});
;
});
});
val treeNode = TreeNode {
name = "Root"
childNodes().addBy {
name = "Food"
childNodes().addBy {
name = "Drinks"
childNodes().addBy {
name = "Cocacola"
}
childNodes().addBy {
name = "Fanta"
}
}
}
}
3. Create new data based on existing data
- Java
- Kotlin
TreeNode newTreeNode = Immutables.createTreeNode(
treeNode, // existing data
root -> {
root
.childNodes(false).get(0) // Food
.childNodes(false).get(0) // Drink
.childNodes(false).get(0) // Cocacola
.setName("Cocacola plus");
}
);
// Show that `newTreeNode` reflects the developer's desired modifications
// Note that this does not affect the existing `treeNode` at all
System.out.println("treeNode:" + treeNode);
System.out.println("newTreeNode:" + newTreeNode);
/*
* val newTreeNode = treeNode.copy {
* ...
* }
*
* is actually shorthand for
*
* val newTreeNode = TreeNode(treeNode) {
* ...
* }
*/
val newTreeNode = treeNode.copy {
childNodes()[0] // Food
.childNodes()[0] // Drinks
.childNodes()[0] // Cocacola
.name += " plus"
}
// Show that `newTreeNode` reflects the developer's desired modifications
// Note that this does not affect the existing `treeNode` at all
println("treeNode: $treeNode")
println("newTreeNode: $newTreeNode")
Output (the actual printed output is compact, but is formatted here for readability)
treeNode: {
"name":"Root",
"childNodes":[
{
"name":"Food",
"childNodes":[
{
"name":"Drink",
"childNodes":[
{"name":"Coco Cola"},
{"name":"Fanta"}
]
}
]
}
]
}
newTreeNode: {
"name":"Root",
"childNodes":[
{
"name":"Food",
"childNodes":[
{
"name":"Drink",
"childNodes":[
{"name":"Coco Cola plus"},
{"name":"Fanta"}
]
}
]
}
]
}
As you can see, treeNode
is unaffected, and newTreeNode
reflects the user's desired modifications.
This transplant is a powerful complement to the JVM ecosystem.
This framework is named Jimmer, paying tribute to immer.
The sample code above uses a type called TreeNodeDraft
, which is the interface type automatically generated by Jimmer based on the user-defined type TreeNode
.
Readers can ignore this auto-generated interface for now, later documentation Draft will introduce it.